diff --git a/.config/guardian/.gdnsuppress b/.config/guardian/.gdnsuppress deleted file mode 100644 index 6f6833c5422..00000000000 --- a/.config/guardian/.gdnsuppress +++ /dev/null @@ -1,76 +0,0 @@ -{ - "hydrated": false, - "properties": { - "helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/suppressions" - }, - "version": "1.0.0", - "suppressionSets": { - "default": { - "name": "default", - "createdDate": "2025-03-17 11:52:32Z", - "lastUpdatedDate": "2025-08-06 13:58:56Z" - } - }, - "results": { - "216e2ac9cb596796224b47799f656570a01fa0d9b5f935608b47d15ab613c8e8": { - "signature": "216e2ac9cb596796224b47799f656570a01fa0d9b5f935608b47d15ab613c8e8", - "alternativeSignatures": [ - "07746898f43afab7cc50931b33154c2d9e1a35f82a649dbe8aecf785b3d5a813" - ], - "memberOf": [ - "default" - ], - "createdDate": "2025-03-17 11:52:32Z" - }, - "77797a3e44634bb2994bd13ccc95ff4575bba474585dbd2cf3068a1c16bc0624": { - "signature": "77797a3e44634bb2994bd13ccc95ff4575bba474585dbd2cf3068a1c16bc0624", - "alternativeSignatures": [ - "4a6cb67bd4b401e9669c13a2162660aaefc0a94a4122e5b50c198414db545672" - ], - "memberOf": [ - "default" - ], - "createdDate": "2025-03-17 11:52:32Z" - }, - "30418bcc5269eaeb2832a2404465784431d4e72a2af332320c2b1db4768902ad": { - "signature": "30418bcc5269eaeb2832a2404465784431d4e72a2af332320c2b1db4768902ad", - "alternativeSignatures": [ - "b7b9eb974d7d3a4ae14df8695ca5a62592c8c9d20b7eda70a6535d50cbda3e7f" - ], - "memberOf": [ - "default" - ], - "createdDate": "2025-03-17 11:52:32Z" - }, - "9d60fae9db4b8d511637e4a0f902820fbabf962c64ce2b2b8c8ae54c0c06d3ab": { - "signature": "9d60fae9db4b8d511637e4a0f902820fbabf962c64ce2b2b8c8ae54c0c06d3ab", - "alternativeSignatures": [ - "d06382c4909cfa81370526b06d4c47ebdf4425fc0b36053d3f457d5cdf5df8a8" - ], - "memberOf": [ - "default" - ], - "createdDate": "2025-08-06 13:58:56Z" - }, - "7f0626fd14d60d2810a8ddfc1e9fdf1563b991dc8e1ac5880eca42449f752e90": { - "signature": "7f0626fd14d60d2810a8ddfc1e9fdf1563b991dc8e1ac5880eca42449f752e90", - "alternativeSignatures": [ - "6d3dc1d67e5413347520202ce038daf52825c58099670688103db2661facf187" - ], - "memberOf": [ - "default" - ], - "createdDate": "2025-08-06 13:58:56Z" - }, - "40121a40ac42fef69ebcb2b8c2ec7ee659c8d10bc7ab4e95d2a290a48b3d281f": { - "signature": "40121a40ac42fef69ebcb2b8c2ec7ee659c8d10bc7ab4e95d2a290a48b3d281f", - "alternativeSignatures": [ - "3f4bc3f870aa2c71232dd65907522d59a1964c148e373105dec71e0d3da9427f" - ], - "memberOf": [ - "default" - ], - "createdDate": "2025-08-06 13:58:56Z" - } - } -} diff --git a/.eslint-ignore b/.eslint-ignore index e493198185e..6f55ce40f69 100644 --- a/.eslint-ignore +++ b/.eslint-ignore @@ -10,6 +10,7 @@ **/extensions/markdown-language-features/media/** **/extensions/markdown-language-features/notebook-out/** **/extensions/markdown-math/notebook-out/** +**/extensions/mermaid-chat-features/chat-webview-out/** **/extensions/notebook-renderers/renderer-out/index.js **/extensions/simple-browser/media/index.js **/extensions/terminal-suggest/src/completions/upstream/** diff --git a/.eslint-plugin-local/code-no-observable-get-in-reactive-context.ts b/.eslint-plugin-local/code-no-observable-get-in-reactive-context.ts new file mode 100644 index 00000000000..699b1994595 --- /dev/null +++ b/.eslint-plugin-local/code-no-observable-get-in-reactive-context.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/utils'; +import * as ESTree from 'estree'; +import * as visitorKeys from 'eslint-visitor-keys'; + +export = new class NoObservableGetInReactiveContext implements eslint.Rule.RuleModule { + meta: eslint.Rule.RuleMetaData = { + type: 'problem', + docs: { + description: 'Disallow calling .get() on observables inside reactive contexts in favor of .read(undefined).', + }, + fixable: 'code', + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + return { + 'CallExpression': (node: any) => { + const callExpression = node as TSESTree.CallExpression; + + if (!isReactiveFunctionWithReader(callExpression.callee)) { + return; + } + + const functionArg = callExpression.arguments.find(arg => + arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression' + ) as TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | undefined; + + if (!functionArg) { + return; + } + + const readerName = getReaderParameterName(functionArg); + if (!readerName) { + return; + } + + checkFunctionForObservableGetCalls(functionArg, readerName, context); + } + }; + } +}; + +function checkFunctionForObservableGetCalls( + fn: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, + readerName: string, + context: eslint.Rule.RuleContext +) { + const visited = new Set(); + + function traverse(node: TSESTree.Node) { + if (visited.has(node)) { + return; + } + visited.add(node); + + if (node.type === 'CallExpression' && isObservableGetCall(node)) { + // Flag .get() calls since we're always in a reactive context here + context.report({ + node: node as any as ESTree.Node, + message: `Observable '.get()' should not be used in reactive context. Use '.read(${readerName})' instead to properly track dependencies or '.read(undefined)' to be explicit about an untracked read.`, + fix: (fixer) => { + const memberExpression = node.callee as TSESTree.MemberExpression; + return fixer.replaceText(node as any, `${context.getSourceCode().getText(memberExpression.object as any)}.read(undefined)`); + } + }); + } + + walkChildren(node, traverse); + } + + if (fn.body) { + traverse(fn.body); + } +} + +function isObservableGetCall(node: TSESTree.CallExpression): boolean { + // Look for pattern: something.get() + if (node.callee.type === 'MemberExpression' && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'get' && + node.arguments.length === 0) { + + // This is a .get() call with no arguments, which is likely an observable + return true; + } + return false; +} + +const reactiveFunctions = new Set([ + 'derived', + 'derivedDisposable', + 'derivedHandleChanges', + 'derivedOpts', + 'derivedWithSetter', + 'derivedWithStore', + 'autorun', + 'autorunOpts', + 'autorunHandleChanges', + 'autorunSelfDisposable', + 'autorunDelta', + 'autorunWithStore', + 'autorunWithStoreHandleChanges', + 'autorunIterableDelta' +]); + +function getReaderParameterName(fn: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression): string | null { + if (fn.params.length === 0) { + return null; + } + const firstParam = fn.params[0]; + if (firstParam.type === 'Identifier') { + // Accept any parameter name as a potential reader parameter + // since reactive functions should always have the reader as the first parameter + return firstParam.name; + } + return null; +} + +function isReactiveFunctionWithReader(callee: TSESTree.Node): boolean { + if (callee.type === 'Identifier') { + return reactiveFunctions.has(callee.name); + } + return false; +} + +function walkChildren(node: TSESTree.Node, cb: (child: TSESTree.Node) => void) { + const keys = visitorKeys.KEYS[node.type] || []; + for (const key of keys) { + const child = (node as any)[key]; + if (Array.isArray(child)) { + for (const item of child) { + if (item && typeof item === 'object' && item.type) { + cb(item); + } + } + } else if (child && typeof child === 'object' && child.type) { + cb(child); + } + } +} diff --git a/.eslint-plugin-local/code-no-reader-after-await.ts b/.eslint-plugin-local/code-no-reader-after-await.ts new file mode 100644 index 00000000000..b8c39969c5b --- /dev/null +++ b/.eslint-plugin-local/code-no-reader-after-await.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/utils'; +import * as ESTree from 'estree'; + +export = new class NoReaderAfterAwait implements eslint.Rule.RuleModule { + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + return { + 'CallExpression': (node: any) => { + const callExpression = node as TSESTree.CallExpression; + + if (!isFunctionWithReader(callExpression.callee)) { + return; + } + + const functionArg = callExpression.arguments.find(arg => + arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression' + ) as TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression | undefined; + + if (!functionArg) { + return; + } + + const readerName = getReaderParameterName(functionArg); + if (!readerName) { + return; + } + + checkFunctionForAwaitBeforeReader(functionArg, readerName, context); + } + }; + } +}; + +function checkFunctionForAwaitBeforeReader( + fn: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression, + readerName: string, + context: eslint.Rule.RuleContext +) { + const awaitPositions: { line: number; column: number }[] = []; + const visited = new Set(); + + function collectPositions(node: TSESTree.Node) { + if (visited.has(node)) { + return; + } + visited.add(node); + + if (node.type === 'AwaitExpression') { + awaitPositions.push({ + line: node.loc?.start.line || 0, + column: node.loc?.start.column || 0 + }); + } else if (node.type === 'CallExpression' && isReaderMethodCall(node, readerName)) { + if (awaitPositions.length > 0) { + const methodName = getMethodName(node); + context.report({ + node: node as any as ESTree.Node, + message: `Reader method '${methodName}' should not be called after 'await'. The reader becomes invalid after async operations.` + }); + } + } + + // Safely traverse known node types only + switch (node.type) { + case 'BlockStatement': + node.body.forEach(stmt => collectPositions(stmt)); + break; + case 'ExpressionStatement': + collectPositions(node.expression); + break; + case 'VariableDeclaration': + node.declarations.forEach(decl => { + if (decl.init) { collectPositions(decl.init); } + }); + break; + case 'AwaitExpression': + if (node.argument) { collectPositions(node.argument); } + break; + case 'CallExpression': + node.arguments.forEach(arg => collectPositions(arg)); + break; + case 'IfStatement': + collectPositions(node.test); + collectPositions(node.consequent); + if (node.alternate) { collectPositions(node.alternate); } + break; + case 'TryStatement': + collectPositions(node.block); + if (node.handler) { collectPositions(node.handler.body); } + if (node.finalizer) { collectPositions(node.finalizer); } + break; + case 'ReturnStatement': + if (node.argument) { collectPositions(node.argument); } + break; + case 'BinaryExpression': + case 'LogicalExpression': + collectPositions(node.left); + collectPositions(node.right); + break; + case 'MemberExpression': + collectPositions(node.object); + if (node.computed) { collectPositions(node.property); } + break; + case 'AssignmentExpression': + collectPositions(node.left); + collectPositions(node.right); + break; + } + } + + if (fn.body) { + collectPositions(fn.body); + } +} + +function getMethodName(callExpression: TSESTree.CallExpression): string { + if (callExpression.callee.type === 'MemberExpression' && + callExpression.callee.property.type === 'Identifier') { + return callExpression.callee.property.name; + } + return 'read'; +} + +function isReaderMethodCall(node: TSESTree.CallExpression, readerName: string): boolean { + if (node.callee.type === 'MemberExpression') { + // Pattern 1: reader.read() or reader.readObservable() + if (node.callee.object.type === 'Identifier' && + node.callee.object.name === readerName && + node.callee.property.type === 'Identifier') { + return ['read', 'readObservable'].includes(node.callee.property.name); + } + + // Pattern 2: observable.read(reader) or observable.readObservable(reader) + if (node.callee.property.type === 'Identifier' && + ['read', 'readObservable'].includes(node.callee.property.name)) { + // Check if the reader is passed as the first argument + return node.arguments.length > 0 && + node.arguments[0].type === 'Identifier' && + node.arguments[0].name === readerName; + } + } + return false; +} + +const readerFunctions = new Set(['derived', 'autorun', 'autorunOpts', 'autorunHandleChanges', 'autorunSelfDisposable']); + +function getReaderParameterName(fn: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression): string | null { + if (fn.params.length === 0) { + return null; + } + const firstParam = fn.params[0]; + if (firstParam.type === 'Identifier') { + return firstParam.name; + } + return null; +} + +function isFunctionWithReader(callee: TSESTree.Node): boolean { + if (callee.type === 'Identifier') { + return readerFunctions.has(callee.name); + } + return false; +} diff --git a/.eslint-plugin-local/tests/code-no-observable-get-in-reactive-context-test.ts b/.eslint-plugin-local/tests/code-no-observable-get-in-reactive-context-test.ts new file mode 100644 index 00000000000..fd92a45e22b --- /dev/null +++ b/.eslint-plugin-local/tests/code-no-observable-get-in-reactive-context-test.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Test file to verify the code-no-observable-get-in-reactive-context ESLint rule works correctly + +import { observableValue, derived, autorun } from '../../src/vs/base/common/observable.js'; + +export function testValidUsage() { + const obs = observableValue('test', 0); + + // Valid: Using .read(reader) in derived + const validDerived = derived(reader => { + const value = obs.read(reader); + return value * 2; + }); + + // Valid: Using .read(reader) in autorun + autorun(rdr => { + const value = validDerived.read(rdr); + console.log('Value:', value); + }); + + // Valid: Using .get() outside reactive context + const outsideValue = obs.get(); + console.log('Outside value:', outsideValue); +} + +export function testInvalidUsage() { + const obs = observableValue('test', 0); + + // Invalid: Using .get() in derived instead of .read(reader) + const invalidDerived = derived(rdr => { + // This should use obs.read(reader) instead + // eslint-disable-next-line local/code-no-observable-get-in-reactive-context + const value = obs.get(); + // Use reader for something valid to avoid unused var warning + const validValue = obs.read(rdr); + + obs.read(undefined); + + return value * 2 + validValue; + }); + + // Invalid: Using .get() in autorun instead of .read(reader) + autorun(reader => { + // This should use invalidDerived.read(reader) instead + // eslint-disable-next-line local/code-no-observable-get-in-reactive-context + const value = invalidDerived.get(); + // Use reader for something valid to avoid unused var warning + const validValue = obs.read(reader); + console.log('Value:', value, validValue); + }); + + // Invalid: Using .get() in derivedWithStore + derived(reader => { + // eslint-disable-next-line local/code-no-observable-get-in-reactive-context + const value = obs.get(); + reader.store.add({ dispose: () => { } }); + return value; + }); +} + +export function testComplexCases() { + const obs1 = observableValue('test1', 0); + const obs2 = observableValue('test2', 10); + + // Invalid: Using .get() in conditional within derived + derived(reader => { + const initial = obs1.read(reader); + + if (initial > 0) { + // eslint-disable-next-line local/code-no-observable-get-in-reactive-context + return obs2.get(); + } + + return initial; + }); + + // Invalid: Using .get() in nested function call within autorun + autorun(reader => { + const process = () => { + // eslint-disable-next-line local/code-no-observable-get-in-reactive-context + return obs1.get() + obs2.get(); + }; + + // Use reader for something valid to avoid unused var warning + const validValue = obs1.read(reader); + const result = process(); + console.log('Result:', result, validValue); + }); + + // Invalid: Using .get() in try-catch within derived + derived(reader => { + try { + // eslint-disable-next-line local/code-no-observable-get-in-reactive-context + const value = obs1.get(); + // Use reader for something valid to avoid unused var warning + const validValue = obs2.read(reader); + return value * 2 + validValue; + } catch (e) { + return obs2.read(reader); + } + }); +} + +export function testValidComplexCases() { + const obs1 = observableValue('test1', 0); + const obs2 = observableValue('test2', 10); + + // Valid: Proper usage with .read(reader) + derived(reader => { + const value1 = obs1.read(reader); + const value2 = obs2.read(undefined); + + if (value1 > 0) { + return value2; + } + + return value1; + }); + + // Valid: Using .get() outside reactive context + function processValues() { + const val1 = obs1.get(); + const val2 = obs2.get(); + return val1 + val2; + } + + // Valid: Mixed usage - .read(reader) inside reactive, .get() outside + autorun(reader => { + const reactiveValue = obs1.read(reader); + const outsideValue = processValues(); + console.log('Values:', reactiveValue, outsideValue); + }); +} + +export function testEdgeCases() { + const obs = observableValue('test', 0); + + // Valid: Function with no reader parameter + derived(() => { + const value = obs.get(); + return value; + }); + + // Invalid: Function with differently named parameter (now also flagged) + derived(_someOtherName => { + // eslint-disable-next-line local/code-no-observable-get-in-reactive-context + const value = obs.get(); + return value; + }); + + // Invalid: Correctly named reader parameter + derived(reader => { + // eslint-disable-next-line local/code-no-observable-get-in-reactive-context + const value = obs.get(); + // Use reader for something valid to avoid unused var warning + const validValue = obs.read(reader); + return value + validValue; + }); +} + +export function testQuickFixScenarios() { + const obs = observableValue('test', 0); + const obs2 = observableValue('test2', 10); + + // These examples show what the quick fix should transform: + + // Example 1: Simple case with 'reader' parameter name + derived(_reader => { + const value = obs.read(undefined); // This should be the auto-fix result + return value; + }); + + // Example 2: Different parameter name + derived(rdr => { + // Before fix: obs2.get() + // After fix: obs2.read(rdr) + const value = obs2.read(rdr); // This should be the auto-fix result + return value; + }); + + // Example 3: Complex expression + derived(ctx => { + // Before fix: (someCondition ? obs : obs2).get() + // After fix: (someCondition ? obs : obs2).read(ctx) + const someCondition = true; + const value = (someCondition ? obs : obs2).read(ctx); // This should be the auto-fix result + return value; + }); + + // Example 4: Multiple calls in same function + autorun(reader => { + // Before fix: obs.get() and obs2.get() + // After fix: obs.read(reader) and obs2.read(reader) + const val1 = obs.read(reader); // This should be the auto-fix result + const val2 = obs2.read(reader); // This should be the auto-fix result + console.log(val1, val2); + }); +} diff --git a/.eslint-plugin-local/tests/code-no-reader-after-await-test.ts b/.eslint-plugin-local/tests/code-no-reader-after-await-test.ts new file mode 100644 index 00000000000..2dc0f457a7f --- /dev/null +++ b/.eslint-plugin-local/tests/code-no-reader-after-await-test.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Test file to verify the code-no-reader-after-await ESLint rule works correctly + +import { observableValue, derived, autorun } from '../../src/vs/base/common/observable.js'; + +export function testValidUsage() { + const obs = observableValue('test', 0); + + const validDerived = derived(reader => { + const value = obs.read(reader); + return value * 2; + }); + + autorun(reader => { + const value = validDerived.read(reader); + console.log('Value:', value); + }); +} + +export function testInvalidUsage() { + const obs = observableValue('test', 0); + + const invalidDerived = derived(async reader => { + await Promise.resolve(); + // eslint-disable-next-line local/code-no-reader-after-await + const value = obs.read(reader); + return value * 2; + }); + + autorun(async reader => { + await Promise.resolve(); + // eslint-disable-next-line local/code-no-reader-after-await + const value = invalidDerived.read(reader); + console.log('Value:', value); + }); + + autorun(async reader => { + await Promise.resolve(); + // eslint-disable-next-line local/code-no-reader-after-await + const value = reader.readObservable(obs); + console.log('Value:', value); + }); +} + +export function testComplexCases() { + const obs = observableValue('test', 0); + + derived(async reader => { + const initial = obs.read(reader); + + if (initial > 0) { + await Promise.resolve(); + } + + // eslint-disable-next-line local/code-no-reader-after-await + const final = obs.read(reader); + return final; + }); + + autorun(async reader => { + try { + await Promise.resolve(); + } catch (e) { + } finally { + // eslint-disable-next-line local/code-no-reader-after-await + const value = obs.read(reader); + console.log(value); + } + }); +} + +export function testValidComplexCases() { + const obs = observableValue('test', 0); + + derived(async reader => { + const value1 = obs.read(reader); + const value2 = reader.readObservable(obs); + const result = value1 + value2; + await Promise.resolve(result); + return result; + }); +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ad5317637ff..311095aa73f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -38,4 +38,4 @@ src/vs/workbench/services/workingCopy/** @bpasero # ensure the API police is aware of changes to the vscode-dts file # this is only about the final API, not about proposed API changes -src/vscode-dts/vscode.d.ts @jrieken @mjbvz +src/vscode-dts/vscode.d.ts @jrieken @mjbvz @alexr00 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a17472617fc..4c7aff83c70 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -61,8 +61,7 @@ You MUST check compilation output before running ANY script or declaring work co - Start the task if it's not already running in the background ### TypeScript validation steps -- Use run test tool or `scripts/test.sh` (`scripts\test.bat` on Windows) for unit tests (add `--grep ` to filter tests) -- Use `scripts/test-integration.sh` (or `scripts\test-integration.bat` on Windows) for integration tests +- Use the run test tool if you need to run tests. If that tool is not available, then you can use `scripts/test.sh` (or `scripts\test.bat` on Windows) for unit tests (add `--grep ` to filter tests) or `scripts/test-integration.sh` (or `scripts\test-integration.bat` on Windows) for integration tests (integration tests end with .integrationTest.ts or are in /extensions/). - Use `npm run valid-layers-check` to check for layering issues ## Coding Guidelines @@ -92,7 +91,8 @@ We use tabs, not spaces. - Use "double quotes" for strings shown to the user that need to be externalized (localized) - Use 'single quotes' otherwise -- All strings visible to the user need to be externalized +- All strings visible to the user need to be externalized using the `vs/nls` module +- Externalized strings must not use string concatenation. Use placeholders instead (`{0}`). ### UI labels - Use title-style capitalization for command labels, buttons and menu items (each word is capitalized). diff --git a/.github/instructions/disposable.instructions.md b/.github/instructions/disposable.instructions.md new file mode 100644 index 00000000000..06e1b62f7e3 --- /dev/null +++ b/.github/instructions/disposable.instructions.md @@ -0,0 +1,20 @@ +--- +description: Guidelines for writing code using IDisposable +--- + +Core symbols: +* `IDisposable` + * `dispose(): void` - dispose the object +* `Disposable` (implements `IDisposable`) - base class for disposable objects + * `this._store: DisposableStore` + * `this._register(t: T): T` + * Try to immediately register created disposables! E.g. `const someDisposable = this._register(new SomeDisposable())` +* `DisposableStore` (implements `IDisposable`) + * `add(t: T): T` + * `clear()` +* `toDisposable(fn: () => void): IDisposable` - helper to create a disposable from a function + +* `MutableDisposable` (implements `IDisposable`) + * `value: IDisposable | undefined` + * `clear()` + * A value that enters a mutable disposable (at least once) will be disposed the latest when the mutable disposable is disposed (or when the value is replaced or cleared). diff --git a/.github/instructions/learnings.instructions.md b/.github/instructions/learnings.instructions.md new file mode 100644 index 00000000000..78a9f52a06e --- /dev/null +++ b/.github/instructions/learnings.instructions.md @@ -0,0 +1,32 @@ +--- +applyTo: ** +description: This document describes how to deal with learnings that you make. (meta instruction) +--- + +This document describes how to deal with learnings that you make. +It is a meta-instruction file. + +Structure of learnings: +* Each instruction file has a "Learnings" section. +* Each learning has a counter that indicates how often that learning was useful (initially 1). +* Each learning has a 1-4 sentences description of the learning. + +Example: +```markdown +## Learnings +* Prefer `const` over `let` whenever possible (1) +* Avoid `any` type (3) +``` + +When the user tells you "learn!", you should: +* extract a learning from the recent conversation + * identify the problem that you created + * identify why it was a problem + * identify how you were told to fix it/how the user fixed it +* create a learning (1-4 sentences) from that + * Write this out to the user and reflect over these sentences + * then, add the reflected learning to the "Learnings" section of the most appropriate instruction file + + + Important: Whenever a learning was really useful, increase the counter!! + When a learning was not useful and just caused more problems, decrease the counter. diff --git a/.github/instructions/observables.instructions.md b/.github/instructions/observables.instructions.md new file mode 100644 index 00000000000..2aedc290be1 --- /dev/null +++ b/.github/instructions/observables.instructions.md @@ -0,0 +1,72 @@ +--- +description: Guidelines for writing code using observables and deriveds. +--- + +```ts +class MyService extends Disposable { + private _myData1 = observableValue(/* always put `this` here */ this, /* initial value*/ 0); + private _myData2 = observableValue(/* always put `this` here */ this, /* initial value*/ 42); + + // Deriveds can combine/derive from other observables/deriveds + private _myDerivedData = derived(this, reader => { + // Use observable.read(reader) to access the value and track the dependency. + return this._myData1.read(reader) * this._myData2.read(reader); + }); + + private _myDerivedDataWithLifetime = derived(this, reader => { + // The reader.store will get cleared just before the derived is re-evaluated or gets unsubscribed. + return reader.store.add(new SomeDisposable(this._myDerivedData.read(reader))); + }); + + constructor() { + this._register(autorun((reader) => { // like mobx autorun, they run immediately and on change + const data = this._myData1.read(reader); // but you only get the data if you pass in the reader! + + console.log(data); + + // also has reader.store + })) + } + + getData(): number { + return this._myData1.get(); // use get if you don't have a reader, but try to avoid it since the dependency is not tracked. + } + + setData1() { + this._myData1.set(42, undefined); // use set to update the value. The second paramater is the transaction, which is undefined here. + } + + setData2() { + transaction(tx => { + // you can use transaction to batch updates, so they are only notified once. + // Whenever multiple observables are synchronously updated together, use transaction! + this._myData1.set(42, tx); + this._myData2.set(43, tx); + }); + } +} +``` + + +Most important symbols: +* `observableValue` +* `disposableObservableValue` +* `derived` +* `autorun` +* `transaction` +* `observableFromEvent` +* `observableSignalFromEvent` +* `observableSignal(...): IObservable` - use `.trigger(tx)` to trigger a change + + +* Check src\vs\base\common\observableInternal\index.ts for a list of all observable utitilies + + +* Important learnings: + * [1] Avoid glitches + * [2] **Choose the right observable value type:** + * Use `observableValue(owner, initialValue)` for regular values + * Use `disposableObservableValue(owner, initialValue)` when storing disposable values - it automatically disposes the previous value when a new one is set, and disposes the current value when the observable itself is disposed (similar to `MutableDisposable` behavior) + * [3] **Choose the right event observable pattern:** + * Use `observableFromEvent(owner, event, valueComputer)` when you need to track a computed value that changes with the event, and you want updates only when the computed value actually changes + * Use `observableSignalFromEvent(owner, event)` when you need to force re-computation every time the event fires, regardless of value stability. This is important when the computed value might not change but dependent computations need fresh context (e.g., workspace folder changes where the folder array reference might be the same but file path calculations need to be refreshed) diff --git a/.github/instructions/tree-widgets.instructions.md b/.github/instructions/tree-widgets.instructions.md new file mode 100644 index 00000000000..b5df37f75e6 --- /dev/null +++ b/.github/instructions/tree-widgets.instructions.md @@ -0,0 +1,157 @@ +--- +description: Use when asked to consume workbench tree widgets in VS Code. +--- + +# Workbench Tree Widgets Overview + +**Location**: `src/vs/platform/list/browser/listService.ts` +**Type**: Platform Services +**Layer**: Platform + +## Purpose + +The Workbench Tree Widgets provide high-level, workbench-integrated tree components that extend the base tree implementations with VS Code-specific functionality like context menus, keyboard navigation, theming, accessibility, and dependency injection integration. These widgets serve as the primary tree components used throughout the VS Code workbench for file explorers, debug views, search results, and other hierarchical data presentations. + +## Scope + +### Included Functionality +- **Context Integration**: Automatic context key management, focus handling, and VS Code theme integration +- **Resource Navigation**: Built-in support for opening files and resources with proper editor integration +- **Accessibility**: Complete accessibility provider integration with screen reader support +- **Keyboard Navigation**: Smart keyboard navigation with search-as-you-type functionality +- **Multi-selection**: Configurable multi-selection behavior with platform-appropriate modifier keys +- **Dependency Injection**: Full integration with VS Code's service container for automatic service injection +- **Configuration**: Automatic integration with user settings for tree behavior customization + +### Integration Points +- **IInstantiationService**: For service injection and component creation +- **IContextKeyService**: For managing focus, selection, and tree state context keys +- **IListService**: For registering trees and managing workbench list lifecycle +- **IConfigurationService**: For reading tree configuration settings +- **Resource Navigators**: For handling file/resource opening with proper editor integration + +### Out of Scope +- Low-level tree rendering and virtualization (handled by base tree classes) +- Data management and async loading logic (provided by data sources) +- Custom styling beyond workbench theming integration + +## Architecture + +### Key Classes & Interfaces + +- **WorkbenchTreeInternals**: Encapsulates common workbench functionality across all tree types +- **ResourceNavigator**: Handles file/resource opening with proper editor integration +- **IOpenEvent**: Event interface for resource opening with editor options +- **IWorkbench*TreeOptions**: Configuration interfaces extending base options with workbench features +- **IResourceNavigatorOptions**: Configuration for resource opening behavior + +### Key Files + +- **`src/vs/platform/list/browser/listService.ts`**: Contains all workbench tree widget implementations, shared workbench functionality (`WorkbenchTreeInternals`), and configuration utilities + - `src/vs/platform/list/browser/test/listService.test.ts`: Unit tests for workbench trees +- **`src/vs/base/browser/ui/tree/objectTree.ts`**: Base implementation for static trees and compressible trees + - `src/vs/base/test/browser/ui/tree/objectTree.test.ts`: Base tree tests +- **`src/vs/base/browser/ui/tree/asyncDataTree.ts`**: Base implementation for async trees with lazy loading support + - `src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts`: Async tree tests +- **`src/vs/base/browser/ui/tree/dataTree.ts`**: Base implementation for data-driven trees with explicit data sources + - `src/vs/base/test/browser/ui/tree/dataTree.test.ts`: Data tree tests +- **`src/vs/base/browser/ui/tree/abstractTree.ts`**: Base tree foundation +- **`src/vs/base/browser/ui/tree/tree.ts`**: Core interfaces and types + +## Development Guidelines + +### Choosing the Right Tree Widget + +1. **WorkbenchObjectTree**: Use for simple, static hierarchical data that doesn't change frequently + ```typescript + // Example: Timeline items, loaded scripts + const tree = instantiationService.createInstance( + WorkbenchObjectTree, + 'TimelineView', container, delegate, renderers, options + ); + ``` + +2. **WorkbenchAsyncDataTree**: Use for dynamic data that loads asynchronously + ```typescript + // Example: Debug variables, file contents + const tree = instantiationService.createInstance( + WorkbenchAsyncDataTree, + 'VariablesView', container, delegate, renderers, dataSource, options + ); + ``` + +3. **WorkbenchCompressible*Tree**: Use when you need path compression for deep hierarchies + ```typescript + // Example: File explorer, call stack + const tree = instantiationService.createInstance( + WorkbenchCompressibleAsyncDataTree, + 'FileExplorer', container, delegate, compressionDelegate, renderers, dataSource, options + ); + ``` + +### Construction Pattern + +**Always use IInstantiationService.createInstance()** to ensure proper dependency injection: + +```typescript +constructor( + @IInstantiationService private instantiationService: IInstantiationService +) { + this.tree = this.instantiationService.createInstance( + WorkbenchAsyncDataTree, + 'UniqueTreeId', // Used for settings and context keys + container, // DOM container element + delegate, // IListVirtualDelegate for item height/template + renderers, // Array of tree renderers + dataSource, // Data source (async trees only) + options // Tree configuration options + ); +} +``` + +### Required Options + +All workbench trees require an **accessibilityProvider**: +```typescript +const options: IWorkbenchAsyncDataTreeOptions = { + accessibilityProvider: { + getAriaLabel: (element: T) => element.name, + getRole: () => 'treeitem' + } + // ... other options +}; +``` + +### Common Configuration Patterns + +```typescript +// Standard tree setup with search, identity, and navigation +const options = { + accessibilityProvider: new MyAccessibilityProvider(), + identityProvider: { getId: (element) => element.id }, + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: (element) => element.name + }, + multipleSelectionController: { + isSelectionSingleChangeEvent: (e) => e.ctrlKey || e.metaKey, + isSelectionRangeChangeEvent: (e) => e.shiftKey + }, + overrideStyles: this.getLocationBasedColors().listOverrideStyles +}; +``` + +### Lifecycle Management + +- **Always register trees as disposables** in the containing component +- **Use the tree's `setInput()` method** to provide initial data +- **Always call `layout()` when the container initializes and when its size changes** +- **Handle selection and open events** through the tree's event system + +### Performance Considerations + +- Use **compression** for deep hierarchies to reduce DOM nodes +- Implement **efficient data sources** that avoid unnecessary data fetching +- Consider **virtualization settings** for large datasets +- Use **identity providers** for efficient updates and state preservation + +--- diff --git a/.github/prompts/component.prompt.md b/.github/prompts/component.prompt.md new file mode 100644 index 00000000000..9aa2eb6d176 --- /dev/null +++ b/.github/prompts/component.prompt.md @@ -0,0 +1,58 @@ +--- +mode: agent +description: 'Help author a component specification for an agent.' +tools: ['edit', 'search', 'usages', 'vscodeAPI', 'fetch', 'extensions', 'todos'] +--- + + +Your goal is to create a component overview in markdown given the context provided by the user. The overview should include a brief description of the component, its main features, an architectural diagram and layout of important code files and their relationships. The purpose of this overview is to enable a developer to attach it to a feature request and ensure the agent has enough context to make correct code changes without breaking functionality. + + + +# [Component Name] Overview + +**Location**: `src/vs/[path/to/component]` +**Type**: [Service/Contribution/Extension/API/etc.] +**Layer (if applicable)**: [base/platform/editor/workbench/code/server] + +## Purpose + +Brief description of what this component does and why it exists. + +## Scope +- What functionality is included +- What is explicitly out of scope +- Integration points with other components + +## Architecture + +### High-Level Design +[Architectural diagram or description of key patterns used] + +### Key Classes & Interfaces +- **[ClassName]**: Brief description of responsibility +- **[InterfaceName]**: Purpose and main methods +- **[ServiceName]**: Service responsibilities + +### Key Files +List all the key files and a brief description of their purpose: +- **`src/vs/[path/to/component]/[filename.ts]`**: [Purpose and main exports] +- **`src/vs/[path/to/component]/[service.ts]`**: [Service implementation details] +- **`src/vs/[path/to/component]/[contribution.ts]`**: [Workbench contributions] + +## Development Guidelines + +- Reserve a section for any specific development practices or patterns relevant to this component. These will be edited by a developer or agent as needed. + +--- + + + +- **Create** a new overview file if one is not specified: `.components/[component-name].md` +- **Fill** each section with component-specific details +- **Gather** information from the attached context and use available tools if needed to complete your understanding +- **Ask** the user for clarification if you cannot fill out a section with accurate information +- **Use complete file paths** from repository root (e.g., `src/vs/workbench/services/example/browser/exampleService.ts`) +- **Keep** descriptions concise but comprehensive +- **Use file references** instead of code snippets when making references to code as otherwise the code may become outdated + diff --git a/.github/prompts/data.prompt.md b/.github/prompts/data.prompt.md new file mode 100644 index 00000000000..a0956fb7d1d --- /dev/null +++ b/.github/prompts/data.prompt.md @@ -0,0 +1,42 @@ +--- +mode: agent +description: 'Answer telemetry questions with data queries' +tools: ['runInTerminal', 'search', 'extensions', 'githubRepo', 'todos', 'kusto_query'] +--- + + +You are a Azure Data Explorer data analyst with expert knowledge in Kusto Query Language (KQL) and data analysis. Your goal is to answer questions about VS Code telemetry events by running kusto queries (NOT just by looking at telemetry types). + + + +1. Read `vscode-telemetry-docs/.github/copilot-instructions.md` to understand how to access VS Code's telemetry + - If the `vscode-telemetry-docs` folder doesn't exist (just check your workspace_info, no extra tool call needed), run `npm run mixin-telemetry-docs` to clone the telemetry documentation. +2. Analyze data using kusto queries: Don't just describe what could be queried - actually execute Kusto queries to provide real data and insights: + - If the `kusto_query` tool doesn't exist (just check your provided tools, no need to run it!), install the `ms-azuretools.vscode-azure-mcp-server` VS Code extension + - Use the appropriate Kusto cluster and database for the data type + - Always include proper time filtering to limit data volume + - Default to a rolling 28-day window if no specific timeframe is requested + - Format and present the query results clearly to answer the user's question + - Track progress of your kusto analysis using todos + - If kusto queries keep failing (up to 3 repeated attempts of fixing parametersor queries), stop and inform the user. + + + +When writing Kusto queries, follow these best practices: +- **Explore data efficiently.** Use 1d (1-day) time window and `sample` operator to quickly understand data shape and volume +- **Aggregate usage in proper time windows.** When no specific timeframe is provided: + - Default to a rolling 28-day window (standard practice in VS Code telemetry) + - Use full day boundaries to avoid partial day data + - Follow the time filtering patterns from the telemetry documentation +- **Correctly map names and keys.** EventName is the prefix (`monacoworkbench/` for vscode) and lowercase event name. Properties/Measurements keys are lowercase. Any properties marked `isMeasurement` are in the Measurements bag. +- **Parallelize queries when possible.** Run multiple independent queries as parallel tool calls to speed up analysis. + + + +Your response should include: +- The actual Kusto query executed (formatted nicely) +- Real query results with data to answer the user's question +- Interpretation and analysis of the results +- References to specific documentation files when applicable +- Additional context or insights from the telemetry data + diff --git a/.github/prompts/fixIssueNo.prompt.md b/.github/prompts/fixIssueNo.prompt.md new file mode 100644 index 00000000000..6438f4614fe --- /dev/null +++ b/.github/prompts/fixIssueNo.prompt.md @@ -0,0 +1,8 @@ +--- +mode: Plan +tools: ['runCommands', 'runTasks', 'runNotebooks', 'search', 'new', 'usages', 'vscodeAPI', 'problems', 'testFailure', 'openSimpleBrowser', 'fetch', 'githubRepo', 'todos', 'runTests', 'get_issue', 'get_issue_comments', 'get_me', 'get_pull_request', 'get_pull_request_diff', 'get_pull_request_files'] +--- + +The user has given you a Github issue number. Use the `get_issue` to retrieve its details. Understand the issue and propose a solution to solve it. + +NEVER share any thinking process or status updates before you have your solution. diff --git a/.github/prompts/update-instructions.prompt.md b/.github/prompts/update-instructions.prompt.md new file mode 100644 index 00000000000..8ba53454822 --- /dev/null +++ b/.github/prompts/update-instructions.prompt.md @@ -0,0 +1,17 @@ +--- +mode: agent +--- + +Read the changes introduced on the current branch, including BOTH: + +1. Uncommitted workspace modifications (staged and unstaged) +2. Committed changes that are on the current HEAD but not yet in the default upstream branch (e.g. `origin/main`) + +Guidance: + +- First, capture uncommitted diffs (equivalent of `git diff` and `git diff --cached`). +- Then, determine the merge base with the default branch (assume `origin/main` unless configured otherwise) using `git merge-base HEAD origin/main` and diff (`git diff ...HEAD`) to include committed-but-unpushed work. + +After understanding all of these changes, read every instruction file under `.github/instructions` and assess whether any instruction is invalidated. If so, propose minimal, necessary wording updates. If no updates are needed, respond exactly with: `No updates needed`. + +Be concise and conservative: only suggest changes that are absolutely necessary. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000000..b520069a389 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,260 @@ +name: "Copilot Setup Steps" + +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: vscode-large-runners + + # Set the permissions to the lowest permissions possible needed for your steps. + # Copilot will be given its own token for its operations. + permissions: + # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. If you don't clone the repository in your setup steps, Copilot will do this for you automatically after the steps complete. + contents: read + + # You can define any steps you want, and they will run before the agent starts. + # If you do not check out your code, Copilot will do this for you. + steps: + - name: Checkout microsoft/vscode + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version-file: .nvmrc + + - name: Setup system services + run: | + set -e + # Start X server + ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get update + ./build/azure-pipelines/linux/apt-retry.sh sudo apt-get install -y pkg-config \ + xvfb \ + libgtk-3-0 \ + libxkbfile-dev \ + libkrb5-dev \ + libgbm1 \ + rpm + sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb + sudo chmod +x /etc/init.d/xvfb + sudo update-rc.d xvfb defaults + sudo service xvfb start + + - name: Prepare node_modules cache key + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux x64 $(node -p process.arch) > .build/packagelockhash + + - name: Restore node_modules cache + id: cache-node-modules + uses: actions/cache/restore@v4 + with: + path: .build/node_modules_cache + key: "node_modules-linux-${{ hashFiles('.build/packagelockhash') }}" + + - name: Extract node_modules cache + if: steps.cache-node-modules.outputs.cache-hit == 'true' + run: tar -xzf .build/node_modules_cache/cache.tgz + + - name: Install build dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + working-directory: build + run: | + set -e + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + + source ./build/azure-pipelines/linux/setup-env.sh + + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + npm_config_arch: x64 + VSCODE_ARCH: x64 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create node_modules archive + if: steps.cache-node-modules.outputs.cache-hit != 'true' + run: | + set -e + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + + - name: Create .build folder + run: mkdir -p .build + + - name: Prepare built-in extensions cache key + run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + + - name: Restore built-in extensions cache + id: cache-builtin-extensions + uses: actions/cache/restore@v4 + with: + enableCrossOsArchive: true + path: .build/builtInExtensions + key: "builtin-extensions-${{ hashFiles('.build/builtindepshash') }}" + + - name: Download built-in extensions + if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' + run: node build/lib/builtInExtensions.js + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # - name: Transpile client and extensions + # run: npm run gulp transpile-client-esbuild transpile-extensions + + - name: Download Electron and Playwright + run: | + set -e + + for i in {1..3}; do # try 3 times (matching retryCountOnTaskFailure: 3) + if npm exec -- npm-run-all -lp "electron x64" "playwright-install"; then + echo "Download successful on attempt $i" + break + fi + + if [ $i -eq 3 ]; then + echo "Download failed after 3 attempts" >&2 + exit 1 + fi + + echo "Download failed on attempt $i, retrying..." + sleep 5 # optional: add a small delay between retries + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # - name: 🧪 Run unit tests (Electron) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 15 + # run: ./scripts/test.sh --tfs "Unit Tests" + # env: + # DISPLAY: ":10" + + # - name: 🧪 Run unit tests (node.js) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 15 + # run: npm run test-node + + # - name: 🧪 Run unit tests (Browser, Chromium) + # if: ${{ inputs.browser_tests }} + # timeout-minutes: 30 + # run: npm run test-browser-no-install -- --browser chromium --tfs "Browser Unit Tests" + # env: + # DEBUG: "*browser*" + + # - name: Build integration tests + # run: | + # set -e + # npm run gulp \ + # compile-extension:configuration-editing \ + # compile-extension:css-language-features-server \ + # compile-extension:emmet \ + # compile-extension:git \ + # compile-extension:github-authentication \ + # compile-extension:html-language-features-server \ + # compile-extension:ipynb \ + # compile-extension:notebook-renderers \ + # compile-extension:json-language-features-server \ + # compile-extension:markdown-language-features \ + # compile-extension-media \ + # compile-extension:microsoft-authentication \ + # compile-extension:typescript-language-features \ + # compile-extension:vscode-api-tests \ + # compile-extension:vscode-colorize-tests \ + # compile-extension:vscode-colorize-perf-tests \ + # compile-extension:vscode-test-resolver + + # - name: 🧪 Run integration tests (Electron) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 20 + # run: ./scripts/test-integration.sh --tfs "Integration Tests" + # env: + # DISPLAY: ":10" + + # - name: 🧪 Run integration tests (Browser, Chromium) + # if: ${{ inputs.browser_tests }} + # timeout-minutes: 20 + # run: ./scripts/test-web-integration.sh --browser chromium + + # - name: 🧪 Run integration tests (Remote) + # if: ${{ inputs.remote_tests }} + # timeout-minutes: 20 + # run: ./scripts/test-remote-integration.sh + # env: + # DISPLAY: ":10" + + # - name: Compile smoke tests + # working-directory: test/smoke + # run: npm run compile + + # - name: Compile extensions for smoke tests + # run: npm run gulp compile-extension-media + + # - name: Diagnostics before smoke test run (processes, max_user_watches, number of opened file handles) + # run: | + # set -e + # ps -ef + # cat /proc/sys/fs/inotify/max_user_watches + # lsof | wc -l + # continue-on-error: true + # if: always() + + # - name: 🧪 Run smoke tests (Electron) + # if: ${{ inputs.electron_tests }} + # timeout-minutes: 20 + # run: npm run smoketest-no-compile -- --tracing + # env: + # DISPLAY: ":10" + + # - name: 🧪 Run smoke tests (Browser, Chromium) + # if: ${{ inputs.browser_tests }} + # timeout-minutes: 20 + # run: npm run smoketest-no-compile -- --web --tracing --headless + + # - name: 🧪 Run smoke tests (Remote) + # if: ${{ inputs.remote_tests }} + # timeout-minutes: 20 + # run: npm run smoketest-no-compile -- --remote --tracing + # env: + # DISPLAY: ":10" + + # - name: Diagnostics after smoke test run (processes, max_user_watches, number of opened file handles) + # run: | + # set -e + # ps -ef + # cat /proc/sys/fs/inotify/max_user_watches + # lsof | wc -l + # continue-on-error: true + # if: always() diff --git a/.gitignore b/.gitignore index 62394c60784..92971a7a573 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ product.overrides.json *.snap.actual *.tsbuildinfo .vscode-test +vscode-telemetry-docs/ diff --git a/.npmrc b/.npmrc index c8d1ede6bc4..749c3c543e2 100644 --- a/.npmrc +++ b/.npmrc @@ -1,6 +1,6 @@ disturl="https://electronjs.org/headers" target="37.3.1" -ms_build_id="12342881" +ms_build_id="12404162" runtime="electron" build_from_source="true" legacy-peer-deps="true" diff --git a/.vscode/settings.json b/.vscode/settings.json index 9e8ca8f88e1..005d17b5e54 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -211,9 +211,6 @@ ], // --- Workbench --- - "remote.extensionKind": { - "msjsdiag.debugger-for-chrome": "workspace" - }, // "application.experimental.rendererProfiling": true, // https://github.com/microsoft/vscode/issues/265654 "editor.aiStats.enabled": true, // Team selfhosting on ai stats @@ -223,5 +220,11 @@ "chat.promptFilesRecommendations": { "plan-fast": true, "plan-deep": true - } + }, + // Needed for kusto tool in data.prompt.md + "azureMcp.enabledServices": [ + "kusto" + ], + "azureMcp.serverMode": "all", + "azureMcp.readOnly": true } diff --git a/build/azure-pipelines/alpine/cli-build-alpine.yml b/build/azure-pipelines/alpine/cli-build-alpine.yml index 95dee012c06..6180a7efd10 100644 --- a/build/azure-pipelines/alpine/cli-build-alpine.yml +++ b/build/azure-pipelines/alpine/cli-build-alpine.yml @@ -12,6 +12,8 @@ parameters: default: false steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml index 303db76fd0f..a2967162b25 100644 --- a/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/build/azure-pipelines/alpine/product-build-alpine.yml @@ -1,4 +1,6 @@ steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile @@ -70,6 +72,19 @@ steps: displayName: Install build dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + - script: | + set -e + mkdir -p .build/nodejs-musl + NODE_VERSION=$(grep '^target=' remote/.npmrc | cut -d '"' -f 2) + BUILD_ID=$(grep '^ms_build_id=' remote/.npmrc | cut -d '"' -f 2) + gh release download "v${NODE_VERSION}-${BUILD_ID}" -R microsoft/vscode-node -p "node-v${NODE_VERSION}-linux-${VSCODE_ARCH}-musl.tar.gz" --dir .build/nodejs-musl --clobber + tar -xzf ".build/nodejs-musl/node-v${NODE_VERSION}-linux-${VSCODE_ARCH}-musl.tar.gz" -C ".build/nodejs-musl" --strip-components=1 + rm ".build/nodejs-musl/node-v${NODE_VERSION}-linux-${VSCODE_ARCH}-musl.tar.gz" + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Download NodeJS MUSL + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + - script: | set -e @@ -88,6 +103,7 @@ steps: GITHUB_TOKEN: "$(github-distro-mixin-password)" VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME: vscodehub.azurecr.io/vscode-linux-build-agent:alpine-$(VSCODE_ARCH) VSCODE_HOST_MOUNT: "/mnt/vss/_work/1/s" + VSCODE_NPMRC_PATH: $(NPMRC_PATH) displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) diff --git a/build/azure-pipelines/cli/install-rust-posix.yml b/build/azure-pipelines/cli/install-rust-posix.yml index 0607cde33e5..d9bae080cc2 100644 --- a/build/azure-pipelines/cli/install-rust-posix.yml +++ b/build/azure-pipelines/cli/install-rust-posix.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.85 + default: 1.88 - name: targets default: [] type: object diff --git a/build/azure-pipelines/common/checkout.yml b/build/azure-pipelines/common/checkout.yml new file mode 100644 index 00000000000..6f57a9ad9b4 --- /dev/null +++ b/build/azure-pipelines/common/checkout.yml @@ -0,0 +1,5 @@ +steps: + - checkout: self + fetchDepth: 1 + fetchTags: false + displayName: Checkout microsoft/vscode diff --git a/build/azure-pipelines/common/publish-artifact.yml b/build/azure-pipelines/common/publish-artifact.yml index b18dc8d4c7f..ba4d9f13355 100644 --- a/build/azure-pipelines/common/publish-artifact.yml +++ b/build/azure-pipelines/common/publish-artifact.yml @@ -18,9 +18,6 @@ parameters: - name: sbomPackageVersion type: string default: "" - - name: isProduction - type: boolean - default: true - name: condition type: string default: succeeded() @@ -80,7 +77,6 @@ steps: targetPath: ${{ parameters.targetPath }} artifactName: $(ARTIFACT_NAME) sbomEnabled: ${{ parameters.sbomEnabled }} - isProduction: ${{ parameters.isProduction }} ${{ if ne(parameters.sbomBuildDropPath, '') }}: sbomBuildDropPath: ${{ parameters.sbomBuildDropPath }} ${{ if ne(parameters.sbomPackageName, '') }}: diff --git a/build/azure-pipelines/config/CredScanSuppressions.json b/build/azure-pipelines/config/CredScanSuppressions.json index bf52c06cf89..7cb84e9d0ca 100644 --- a/build/azure-pipelines/config/CredScanSuppressions.json +++ b/build/azure-pipelines/config/CredScanSuppressions.json @@ -4,12 +4,19 @@ { "file": [ "src/vs/base/test/common/uri.test.ts", - "src/vs/workbench/api/test/browser/extHostTelemetry.test.ts" + "src/vs/workbench/api/test/browser/extHostTelemetry.test.ts", + "src/vs/base/test/common/yaml.test.ts" ], "_justification": "These are dummy credentials in tests." }, { "file": [ + ".build/linux/deb/amd64/code-amd64/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/deb/amd64/code-amd64/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/deb/armhf/code-armhf/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/deb/armhf/code-armhf/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/deb/arm64/code-arm64/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/deb/arm64/code-arm64/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code/resources/app/extensions/github-authentication/dist/extension.js", @@ -33,6 +40,12 @@ }, { "file": [ + ".build/linux/deb/amd64/code-insiders-amd64/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/deb/amd64/code-insiders-amd64/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/deb/armhf/code-insiders-armhf/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/deb/armhf/code-insiders-armhf/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/deb/arm64/code-insiders-arm64/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/deb/arm64/code-insiders-arm64/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code-insiders/resources/app/extensions/github-authentication/dist/extension.js", @@ -56,6 +69,12 @@ }, { "file": [ + ".build/linux/deb/amd64/code-exploration-amd64/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/deb/amd64/code-exploration-amd64/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/deb/armhf/code-exploration-armhf/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/deb/armhf/code-exploration-armhf/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", + ".build/linux/deb/arm64/code-exploration-arm64/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", + ".build/linux/deb/arm64/code-exploration-arm64/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", ".build/linux/rpm/x86_64/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/emmet/dist/node/emmetNodeMain.js", ".build/linux/rpm/armv7hl/rpmbuild/BUILD/usr/share/code-exploration/resources/app/extensions/github-authentication/dist/extension.js", diff --git a/build/azure-pipelines/darwin/cli-build-darwin.yml b/build/azure-pipelines/darwin/cli-build-darwin.yml index 730918f5da1..fe44f2827fd 100644 --- a/build/azure-pipelines/darwin/cli-build-darwin.yml +++ b/build/azure-pipelines/darwin/cli-build-darwin.yml @@ -12,6 +12,8 @@ parameters: default: false steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile @@ -71,9 +73,7 @@ steps: targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_darwin_x64_cli.zip artifactName: unsigned_vscode_cli_darwin_x64_cli displayName: Publish unsigned_vscode_cli_darwin_x64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code macOS x64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) + sbomEnabled: false - ${{ if eq(parameters.VSCODE_BUILD_MACOS_ARM64, true) }}: - template: ../common/publish-artifact.yml@self @@ -81,6 +81,4 @@ steps: targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_darwin_arm64_cli.zip artifactName: unsigned_vscode_cli_darwin_arm64_cli displayName: Publish unsigned_vscode_cli_darwin_arm64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code macOS arm64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) + sbomEnabled: false diff --git a/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml b/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml index 505534093d0..a622547e2de 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml @@ -5,6 +5,8 @@ parameters: type: boolean steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml index 3d1dfdf8ea3..f2b5e697c4d 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -132,7 +132,6 @@ steps: ${{ else }}: artifactName: crash-dump-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Crash Reports" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -147,7 +146,6 @@ steps: ${{ else }}: artifactName: node-modules-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Node Modules" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -160,7 +158,6 @@ steps: ${{ else }}: artifactName: logs-macos-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Log Files" - isProduction: false sbomEnabled: false continueOnError: true condition: succeededOrFailed() diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml index 60ce6a82954..b1153dc8c8b 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -1,4 +1,6 @@ steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 7d2849681c5..dfb8284426a 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -17,6 +17,8 @@ parameters: default: "" steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/linux/cli-build-linux.yml b/build/azure-pipelines/linux/cli-build-linux.yml index b29c4259433..c79c00ecf89 100644 --- a/build/azure-pipelines/linux/cli-build-linux.yml +++ b/build/azure-pipelines/linux/cli-build-linux.yml @@ -15,6 +15,8 @@ parameters: type: string steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/linux/product-build-linux-test.yml b/build/azure-pipelines/linux/product-build-linux-test.yml index 4e882b78d25..e4dbfecd91b 100644 --- a/build/azure-pipelines/linux/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/product-build-linux-test.yml @@ -147,7 +147,6 @@ steps: ${{ else }}: artifactName: crash-dump-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Crash Reports" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -162,7 +161,6 @@ steps: ${{ else }}: artifactName: node-modules-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Node Modules" - isProduction: false sbomEnabled: false continueOnError: true condition: failed() @@ -175,7 +173,6 @@ steps: ${{ else }}: artifactName: logs-linux-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) displayName: "Publish Log Files" - isProduction: false sbomEnabled: false continueOnError: true condition: succeededOrFailed() diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index f1e92a60c1f..40d9fb7f383 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -22,6 +22,8 @@ parameters: default: "" steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index c0a346333c1..a9d2a39f252 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -98,10 +98,7 @@ variables: - name: VSCODE_PRIVATE_BUILD value: ${{ ne(variables['Build.Repository.Uri'], 'https://github.com/microsoft/vscode.git') }} - name: NPM_REGISTRY - ${{ if in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI') }}: # disable terrapin when in VSCODE_CIBUILD - value: none - ${{ else }}: - value: ${{ parameters.NPM_REGISTRY }} + value: ${{ parameters.NPM_REGISTRY }} - name: CARGO_REGISTRY value: ${{ parameters.CARGO_REGISTRY }} - name: VSCODE_QUALITY @@ -194,13 +191,7 @@ extends: stages: - stage: Compile jobs: - - job: Compile - timeoutInMinutes: 90 - pool: - name: AcesShared - os: macOS - steps: - - template: build/azure-pipelines/product-compile.yml@self + - template: build/azure-pipelines/product-compile.yml@self - ${{ if or(eq(parameters.VSCODE_BUILD_LINUX, true),eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true),eq(parameters.VSCODE_BUILD_LINUX_ARM64, true),eq(parameters.VSCODE_BUILD_ALPINE, true),eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true),eq(parameters.VSCODE_BUILD_MACOS, true),eq(parameters.VSCODE_BUILD_MACOS_ARM64, true),eq(parameters.VSCODE_BUILD_WIN32, true),eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: - stage: CompileCLI @@ -265,8 +256,7 @@ extends: - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: - job: CLIMacOSX64 pool: - name: Azure Pipelines - image: macOS-13 + name: AcesShared os: macOS variables: # todo@connor4312 to diagnose build flakes @@ -282,8 +272,7 @@ extends: - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true)) }}: - job: CLIMacOSARM64 pool: - name: Azure Pipelines - image: macOS-13 + name: AcesShared os: macOS variables: # todo@connor4312 to diagnose build flakes @@ -296,27 +285,17 @@ extends: VSCODE_BUILD_MACOS_ARM64: ${{ parameters.VSCODE_BUILD_MACOS_ARM64 }} - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - - job: CLIWindowsX64 - pool: - name: 1es-windows-2022-x64 - os: windows - steps: - - template: build/azure-pipelines/win32/cli-build-win32.yml@self - parameters: - VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} + - template: build/azure-pipelines/win32/product-build-win32-cli.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_CHECK_ONLY: ${{ variables.VSCODE_CIBUILD }} - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: - - job: CLIWindowsARM64 - pool: - name: 1es-windows-2022-x64 - os: windows - steps: - - template: build/azure-pipelines/win32/cli-build-win32.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} + - template: build/azure-pipelines/win32/product-build-win32-cli.yml@self + parameters: + VSCODE_ARCH: arm64 + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_COMPILE_ONLY, false)) }}: - stage: APIScan @@ -343,88 +322,44 @@ extends: os: windows jobs: - ${{ if eq(variables['VSCODE_CIBUILD'], true) }}: - - job: WindowsElectronTests - displayName: Electron Tests - timeoutInMinutes: 50 - variables: - VSCODE_ARCH: x64 - steps: - - template: build/azure-pipelines/win32/product-build-win32.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_ARCH: x64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_TEST_ARTIFACT_NAME: electron - VSCODE_RUN_ELECTRON_TESTS: true - - job: WindowsBrowserTests - displayName: Browser Tests - timeoutInMinutes: 50 - variables: - VSCODE_ARCH: x64 - steps: - - template: build/azure-pipelines/win32/product-build-win32.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_ARCH: x64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_TEST_ARTIFACT_NAME: browser - VSCODE_RUN_BROWSER_TESTS: true - - job: WindowsRemoteTests - displayName: Remote Tests - timeoutInMinutes: 50 - variables: - VSCODE_ARCH: x64 - steps: - - template: build/azure-pipelines/win32/product-build-win32.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_ARCH: x64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_TEST_ARTIFACT_NAME: remote - VSCODE_RUN_REMOTE_TESTS: true + - template: build/azure-pipelines/win32/product-build-win32-ci.yml@self + parameters: + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_TEST_SUITE: Electron + - template: build/azure-pipelines/win32/product-build-win32-ci.yml@self + parameters: + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_TEST_SUITE: Browser + - template: build/azure-pipelines/win32/product-build-win32-ci.yml@self + parameters: + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_TEST_SUITE: Remote - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32, true)) }}: - - job: Windows - timeoutInMinutes: 120 - variables: + - template: build/azure-pipelines/win32/product-build-win32.yml@self + parameters: VSCODE_ARCH: x64 - templateContext: - sdl: - suppression: - suppressionFile: $(Build.SourcesDirectory)\.config\guardian\.gdnsuppress - steps: - - template: build/azure-pipelines/win32/product-build-win32.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_ARCH: x64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_RUN_ELECTRON_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_BROWSER_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - VSCODE_RUN_REMOTE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - - - job: WindowsCLISign - timeoutInMinutes: 90 - steps: - - template: build/azure-pipelines/win32/product-build-win32-cli-sign.yml@self - parameters: - VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} - VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_ELECTRON_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_BROWSER_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} + VSCODE_RUN_REMOTE_TESTS: ${{ eq(parameters.VSCODE_STEP_ON_IT, false) }} - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }}: - - job: WindowsARM64 - timeoutInMinutes: 90 - variables: + - template: build/azure-pipelines/win32/product-build-win32.yml@self + parameters: VSCODE_ARCH: arm64 - templateContext: - sdl: - suppression: - suppressionFile: $(Build.SourcesDirectory)\.config\guardian\.gdnsuppress - steps: - - template: build/azure-pipelines/win32/product-build-win32.yml@self - parameters: - VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - VSCODE_ARCH: arm64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), or(eq(parameters.VSCODE_BUILD_WIN32, true), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true))) }}: + - template: build/azure-pipelines/win32/product-build-win32-cli-sign.yml@self + parameters: + VSCODE_BUILD_WIN32: ${{ parameters.VSCODE_BUILD_WIN32 }} + VSCODE_BUILD_WIN32_ARM64: ${{ parameters.VSCODE_BUILD_WIN32_ARM64 }} - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_LINUX'], true)) }}: - stage: Linux @@ -676,6 +611,9 @@ extends: displayName: Publish Build steps: - template: build/azure-pipelines/product-publish.yml@self + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_SCHEDULEDBUILD: ${{ variables.VSCODE_SCHEDULEDBUILD }} - ${{ if and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)) }}: - stage: ApproveRelease @@ -691,12 +629,10 @@ extends: - name: skipComponentGovernanceDetection value: true - - ${{ if or(and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)), and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true))) }}: - stage: Release dependsOn: - Publish - - ${{ if and(parameters.VSCODE_RELEASE, eq(variables['VSCODE_PRIVATE_BUILD'], false)) }}: - - ApproveRelease + - ApproveRelease pool: name: 1es-ubuntu-22.04-x64 os: linux diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 19e96932301..c02c4d20dbd 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -1,148 +1,157 @@ -steps: - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc +jobs: + - job: Compile + timeoutInMinutes: 60 + pool: + name: AcesShared + os: macOS + templateContext: + outputs: + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)/compilation.tar.gz + artifactName: Compilation + displayName: Publish compilation artifact + isProduction: false + sbomEnabled: false + steps: + - template: ./common/checkout.yml@self - - template: ./distro/download-distro.yml@self + - task: NodeTool@0 + inputs: + versionSource: fromFile + versionFilePath: .nvmrc - - task: AzureKeyVault@2 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" + - template: ./distro/download-distro.yml@self - - script: node build/setup-npm-registry.js $NPM_REGISTRY - condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Registry + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: vscode + KeyVaultName: vscode-build-secrets + SecretsFilter: "github-distro-mixin-password" - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js compile $(node -p process.arch) > .build/packagelockhash - displayName: Prepare node_modules cache key + - script: node build/setup-npm-registry.js $NPM_REGISTRY + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Registry - - task: Cache@2 - inputs: - key: '"node_modules" | .build/packagelockhash' - path: .build/node_modules_cache - cacheHitVar: NODE_MODULES_RESTORED - displayName: Restore node_modules cache + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js compile $(node -p process.arch) > .build/packagelockhash + displayName: Prepare node_modules cache key - - script: tar -xzf .build/node_modules_cache/cache.tgz - condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Extract node_modules cache + - task: Cache@2 + inputs: + key: '"node_modules" | .build/packagelockhash' + path: .build/node_modules_cache + cacheHitVar: NODE_MODULES_RESTORED + displayName: Restore node_modules cache - - script: | - set -e - # Set the private NPM registry to the global npmrc file - # so that authentication works for subfolders like build/, remote/, extensions/ etc - # which does not have their own .npmrc file - npm config set registry "$NPM_REGISTRY" - echo "##vso[task.setvariable variable=NPMRC_PATH]$(npm config get userconfig)" - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM + - script: tar -xzf .build/node_modules_cache/cache.tgz + condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Extract node_modules cache - - task: npmAuthenticate@0 - inputs: - workingFile: $(NPMRC_PATH) - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication + - script: | + set -e + # Set the private NPM registry to the global npmrc file + # so that authentication works for subfolders like build/, remote/, extensions/ etc + # which does not have their own .npmrc file + npm config set registry "$NPM_REGISTRY" + echo "##vso[task.setvariable variable=NPMRC_PATH]$(npm config get userconfig)" + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM - - script: | - set -e + - task: npmAuthenticate@0 + inputs: + workingFile: $(NPMRC_PATH) + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Authentication - for i in {1..5}; do # try 5 times - npm ci && break - if [ $i -eq 5 ]; then - echo "Npm install failed too many times" >&2 - exit 1 - fi - echo "Npm install failed $i, trying again..." - done - env: - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Install dependencies - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + - script: | + set -e - - script: node build/azure-pipelines/distro/mixin-npm - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Mixin distro node modules + for i in {1..5}; do # try 5 times + npm ci && break + if [ $i -eq 5 ]; then + echo "Npm install failed too many times" >&2 + exit 1 + fi + echo "Npm install failed $i, trying again..." + done + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Install dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: | - set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt - mkdir -p .build/node_modules_cache - tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Create node_modules archive + - script: node build/azure-pipelines/distro/mixin-npm + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Mixin distro node modules - - script: node build/azure-pipelines/distro/mixin-quality - displayName: Mixin distro quality + - script: | + set -e + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Create node_modules archive - - template: common/install-builtin-extensions.yml@self + - script: node build/azure-pipelines/distro/mixin-quality + displayName: Mixin distro quality - - script: npm exec -- npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Compile & Hygiene + - template: common/install-builtin-extensions.yml@self - - script: | - set -e - npm run compile - displayName: Compile smoke test suites (non-OSS) - workingDirectory: test/smoke - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - script: npm exec -- npm-run-all -lp core-ci extensions-ci hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Compile & Hygiene - - script: | - set -e - npm run compile - displayName: Compile integration test suites (non-OSS) - workingDirectory: test/integration/browser - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - script: | + set -e + npm run compile + displayName: Compile smoke test suites (non-OSS) + workingDirectory: test/smoke + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - task: AzureCLI@2 - displayName: Fetch secrets - inputs: - azureSubscription: vscode - scriptType: pscore - scriptLocation: inlineScript - addSpnToEnvironment: true - inlineScript: | - Write-Host "##vso[task.setvariable variable=AZURE_TENANT_ID]$env:tenantId" - Write-Host "##vso[task.setvariable variable=AZURE_CLIENT_ID]$env:servicePrincipalId" - Write-Host "##vso[task.setvariable variable=AZURE_ID_TOKEN;issecret=true]$env:idToken" + - script: | + set -e + npm run compile + displayName: Compile integration test suites (non-OSS) + workingDirectory: test/integration/browser + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - set -e - AZURE_STORAGE_ACCOUNT="vscodeweb" \ - AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ - AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ - AZURE_ID_TOKEN="$(AZURE_ID_TOKEN)" \ - node build/azure-pipelines/upload-sourcemaps - displayName: Upload sourcemaps to Azure + - task: AzureCLI@2 + displayName: Fetch secrets + inputs: + azureSubscription: vscode + scriptType: pscore + scriptLocation: inlineScript + addSpnToEnvironment: true + inlineScript: | + Write-Host "##vso[task.setvariable variable=AZURE_TENANT_ID]$env:tenantId" + Write-Host "##vso[task.setvariable variable=AZURE_CLIENT_ID]$env:servicePrincipalId" + Write-Host "##vso[task.setvariable variable=AZURE_ID_TOKEN;issecret=true]$env:idToken" - - script: ./build/azure-pipelines/common/extract-telemetry.sh - displayName: Generate lists of telemetry events + - script: | + set -e + AZURE_STORAGE_ACCOUNT="vscodeweb" \ + AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ + AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ + AZURE_ID_TOKEN="$(AZURE_ID_TOKEN)" \ + node build/azure-pipelines/upload-sourcemaps + displayName: Upload sourcemaps to Azure - - script: tar -cz --exclude='.build/node_modules_cache' --exclude='.build/node_modules_list.txt' --exclude='.build/distro' -f $(Build.ArtifactStagingDirectory)/compilation.tar.gz $(ls -d .build out-* test/integration/browser/out test/smoke/out test/automation/out 2>/dev/null) - displayName: Compress compilation artifact + - script: ./build/azure-pipelines/common/extract-telemetry.sh + displayName: Generate lists of telemetry events - - template: common/publish-artifact.yml@self - parameters: - targetPath: $(Build.ArtifactStagingDirectory)/compilation.tar.gz - artifactName: Compilation - displayName: Publish compilation artifact - sbomEnabled: false + - script: tar -cz --exclude='.build/node_modules_cache' --exclude='.build/node_modules_list.txt' --exclude='.build/distro' -f $(Build.ArtifactStagingDirectory)/compilation.tar.gz $(ls -d .build out-* test/integration/browser/out test/smoke/out test/automation/out 2>/dev/null) + displayName: Compress compilation artifact - - script: npm run download-builtin-extensions-cg - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Download component details of built-in extensions + - script: npm run download-builtin-extensions-cg + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Download component details of built-in extensions - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: "Component Detection" - inputs: - sourceScanPath: $(Build.SourcesDirectory) - alertWarningLevel: Medium - continueOnError: true + - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 + displayName: "Component Detection" + inputs: + sourceScanPath: $(Build.SourcesDirectory) + alertWarningLevel: Medium + continueOnError: true diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index 90cd06c5459..239d6e854b4 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -1,4 +1,12 @@ +parameters: + - name: VSCODE_QUALITY + type: string + - name: VSCODE_SCHEDULEDBUILD + type: boolean + steps: + - template: ./common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile @@ -93,3 +101,11 @@ steps: displayName: Publish the artifacts processed for this stage attempt sbomEnabled: false condition: always() + + - ${{ if and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(parameters.VSCODE_SCHEDULEDBUILD, true)) }}: + - script: node build/azure-pipelines/common/releaseBuild.js + env: + AZURE_TENANT_ID: "$(AZURE_TENANT_ID)" + AZURE_CLIENT_ID: "$(AZURE_CLIENT_ID)" + AZURE_ID_TOKEN: "$(AZURE_ID_TOKEN)" + displayName: Release build diff --git a/build/azure-pipelines/product-release.yml b/build/azure-pipelines/product-release.yml index d7b51aa8a92..bb54d50fdda 100644 --- a/build/azure-pipelines/product-release.yml +++ b/build/azure-pipelines/product-release.yml @@ -3,6 +3,8 @@ parameters: type: boolean steps: + - template: ./common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index e35af2b87aa..a372a0e8bae 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -1,4 +1,6 @@ steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/win32/cli-build-win32.yml b/build/azure-pipelines/win32/cli-build-win32.yml deleted file mode 100644 index 1914cb7cf6c..00000000000 --- a/build/azure-pipelines/win32/cli-build-win32.yml +++ /dev/null @@ -1,89 +0,0 @@ -parameters: - - name: VSCODE_BUILD_WIN32 - type: boolean - default: false - - name: VSCODE_BUILD_WIN32_ARM64 - type: boolean - default: false - - name: VSCODE_CHECK_ONLY - type: boolean - default: false - - name: VSCODE_QUALITY - type: string - -steps: - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - - - template: ../cli/cli-apply-patches.yml@self - - - task: Npm@1 - displayName: Download openssl prebuilt - inputs: - command: custom - customCommand: pack @vscode-internal/openssl-prebuilt@0.0.11 - customRegistry: useFeed - customFeed: "Monaco/openssl-prebuilt" - workingDir: $(Build.ArtifactStagingDirectory) - - - powershell: | - mkdir $(Build.ArtifactStagingDirectory)/openssl - tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.11.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl - displayName: Extract openssl prebuilt - - - template: ../cli/install-rust-win32.yml@self - parameters: - targets: - - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - - x86_64-pc-windows-msvc - - ${{ if eq(parameters.VSCODE_BUILD_WIN32_ARM64, true) }}: - - aarch64-pc-windows-msvc - - - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - - template: ../cli/cli-compile.yml@self - parameters: - VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} - VSCODE_CLI_TARGET: x86_64-pc-windows-msvc - VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_win32_x64_cli - VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} - VSCODE_CLI_ENV: - OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/openssl/x64-windows-static/lib - OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/x64-windows-static/include - RUSTFLAGS: "-Ctarget-feature=+crt-static -Clink-args=/guard:cf -Clink-args=/CETCOMPAT" - CFLAGS: "/guard:cf /Qspectre" - - - ${{ if eq(parameters.VSCODE_BUILD_WIN32_ARM64, true) }}: - - template: ../cli/cli-compile.yml@self - parameters: - VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} - VSCODE_CLI_TARGET: aarch64-pc-windows-msvc - VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_win32_arm64_cli - VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} - VSCODE_CLI_ENV: - OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm64-windows-static/lib - OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/arm64-windows-static/include - RUSTFLAGS: "-C target-feature=+crt-static -Clink-args=/guard:cf -Clink-args=/CETCOMPAT:NO" - CFLAGS: "/guard:cf /Qspectre" - - - ${{ if not(parameters.VSCODE_CHECK_ONLY) }}: - - ${{ if eq(parameters.VSCODE_BUILD_WIN32_ARM64, true) }}: - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_win32_arm64_cli.zip - artifactName: unsigned_vscode_cli_win32_arm64_cli - displayName: Publish unsigned_vscode_cli_win32_arm64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code Windows arm64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) - - - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_win32_x64_cli.zip - artifactName: unsigned_vscode_cli_win32_x64_cli - displayName: Publish unsigned_vscode_cli_win32_x64_cli artifact - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/cli - sbomPackageName: "VS Code Windows x64 CLI (unsigned)" - sbomPackageVersion: $(Build.SourceVersion) diff --git a/build/azure-pipelines/win32/product-build-win32-ci.yml b/build/azure-pipelines/win32/product-build-win32-ci.yml new file mode 100644 index 00000000000..23df0320ded --- /dev/null +++ b/build/azure-pipelines/win32/product-build-win32-ci.yml @@ -0,0 +1,49 @@ +parameters: + - name: VSCODE_CIBUILD + type: boolean + - name: VSCODE_QUALITY + type: string + - name: VSCODE_TEST_SUITE + type: string + +jobs: + - job: Windows${{ parameters.VSCODE_TEST_SUITE }} + displayName: ${{ parameters.VSCODE_TEST_SUITE }} Tests + timeoutInMinutes: 50 + variables: + VSCODE_ARCH: x64 + templateContext: + outputs: + - output: pipelineArtifact + targetPath: .build/crashes + artifactName: crash-dump-windows-$(VSCODE_ARCH)-${{ lower(parameters.VSCODE_TEST_SUITE) }}-$(System.JobAttempt) + displayName: Publish Crash Reports + sbomEnabled: false + isProduction: false + condition: failed() + - output: pipelineArtifact + targetPath: node_modules + artifactName: node-modules-windows-$(VSCODE_ARCH)-${{ lower(parameters.VSCODE_TEST_SUITE) }}-$(System.JobAttempt) + displayName: Publish Node Modules + sbomEnabled: false + isProduction: false + condition: failed() + - output: pipelineArtifact + targetPath: .build/logs + artifactName: logs-windows-$(VSCODE_ARCH)-${{ lower(parameters.VSCODE_TEST_SUITE) }}-$(System.JobAttempt) + displayName: Publish Log Files + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + steps: + - template: ./steps/product-build-win32-compile.yml@self + parameters: + VSCODE_ARCH: x64 + VSCODE_CIBUILD: ${{ parameters.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} + ${{ if eq(parameters.VSCODE_TEST_SUITE, 'Electron') }}: + VSCODE_RUN_ELECTRON_TESTS: true + ${{ if eq(parameters.VSCODE_TEST_SUITE, 'Browser') }}: + VSCODE_RUN_BROWSER_TESTS: true + ${{ if eq(parameters.VSCODE_TEST_SUITE, 'Remote') }}: + VSCODE_RUN_REMOTE_TESTS: true diff --git a/build/azure-pipelines/win32/product-build-win32-cli-sign.yml b/build/azure-pipelines/win32/product-build-win32-cli-sign.yml index 520931ffa48..4bb29c968d1 100644 --- a/build/azure-pipelines/win32/product-build-win32-cli-sign.yml +++ b/build/azure-pipelines/win32/product-build-win32-cli-sign.yml @@ -4,56 +4,80 @@ parameters: - name: VSCODE_BUILD_WIN32_ARM64 type: boolean -steps: - - task: NodeTool@0 - displayName: "Use Node.js" - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - - - task: AzureKeyVault@2 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" - - - powershell: node build/setup-npm-registry.js $env:NPM_REGISTRY build - condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Registry - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - # Set the private NPM registry to the global npmrc file - # so that authentication works for subfolders like build/, remote/, extensions/ etc - # which does not have their own .npmrc file - exec { npm config set registry "$env:NPM_REGISTRY" } - $NpmrcPath = (npm config get userconfig) - echo "##vso[task.setvariable variable=NPMRC_PATH]$NpmrcPath" - condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM - - - task: npmAuthenticate@0 - inputs: - workingFile: $(NPMRC_PATH) - condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication - - - powershell: | - . azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npm ci } - workingDirectory: build - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - retryCountOnTaskFailure: 5 - displayName: Install build dependencies - - - template: ../cli/cli-win32-sign.yml@self - parameters: - VSCODE_CLI_ARTIFACTS: +jobs: + - job: WindowsCLISign + timeoutInMinutes: 90 + templateContext: + outputParentDirectory: $(Build.ArtifactStagingDirectory) + outputs: - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: - - unsigned_vscode_cli_win32_x64_cli + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)/vscode_cli_win32_x64_cli.zip + artifactName: vscode_cli_win32_x64_cli + displayName: Publish signed artifact with ID vscode_cli_win32_x64_cli + sbomBuildDropPath: $(Build.BinariesDirectory)/sign/unsigned_vscode_cli_win32_x64_cli + sbomPackageName: "VS Code Windows x64 CLI" + sbomPackageVersion: $(Build.SourceVersion) - ${{ if eq(parameters.VSCODE_BUILD_WIN32_ARM64, true) }}: - - unsigned_vscode_cli_win32_arm64_cli + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)/vscode_cli_win32_arm64_cli.zip + artifactName: vscode_cli_win32_arm64_cli + displayName: Publish signed artifact with ID vscode_cli_win32_arm64_cli + sbomBuildDropPath: $(Build.BinariesDirectory)/sign/unsigned_vscode_cli_win32_arm64_cli + sbomPackageName: "VS Code Windows arm64 CLI" + sbomPackageVersion: $(Build.SourceVersion) + steps: + - template: ../common/checkout.yml@self + + - task: NodeTool@0 + displayName: "Use Node.js" + inputs: + versionSource: fromFile + versionFilePath: .nvmrc + + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: vscode + KeyVaultName: vscode-build-secrets + SecretsFilter: "github-distro-mixin-password" + + - powershell: node build/setup-npm-registry.js $env:NPM_REGISTRY build + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Registry + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + # Set the private NPM registry to the global npmrc file + # so that authentication works for subfolders like build/, remote/, extensions/ etc + # which does not have their own .npmrc file + exec { npm config set registry "$env:NPM_REGISTRY" } + $NpmrcPath = (npm config get userconfig) + echo "##vso[task.setvariable variable=NPMRC_PATH]$NpmrcPath" + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM + + - task: npmAuthenticate@0 + inputs: + workingFile: $(NPMRC_PATH) + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Authentication + + - powershell: | + . azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm ci } + workingDirectory: build + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + retryCountOnTaskFailure: 5 + displayName: Install build dependencies + + - template: ./steps/product-build-win32-cli-sign.yml@self + parameters: + VSCODE_CLI_ARTIFACTS: + - ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}: + - unsigned_vscode_cli_win32_x64_cli + - ${{ if eq(parameters.VSCODE_BUILD_WIN32_ARM64, true) }}: + - unsigned_vscode_cli_win32_arm64_cli diff --git a/build/azure-pipelines/win32/product-build-win32-cli.yml b/build/azure-pipelines/win32/product-build-win32-cli.yml new file mode 100644 index 00000000000..c4801ce3746 --- /dev/null +++ b/build/azure-pipelines/win32/product-build-win32-cli.yml @@ -0,0 +1,76 @@ +parameters: + - name: VSCODE_ARCH + type: string + - name: VSCODE_CHECK_ONLY + type: boolean + default: false + - name: VSCODE_QUALITY + type: string + +jobs: + - job: CLIWindows${{ upper(parameters.VSCODE_ARCH) }} + pool: + name: 1es-windows-2022-x64 + os: windows + timeoutInMinutes: 30 + variables: + VSCODE_ARCH: ${{ parameters.VSCODE_ARCH }} + templateContext: + outputs: + - ${{ if not(parameters.VSCODE_CHECK_ONLY) }}: + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)/unsigned_vscode_cli_win32_$(VSCODE_ARCH)_cli.zip + artifactName: unsigned_vscode_cli_win32_$(VSCODE_ARCH)_cli + displayName: Publish unsigned_vscode_cli_win32_$(VSCODE_ARCH)_cli artifact + sbomEnabled: false + isProduction: false + + steps: + - template: ../common/checkout.yml@self + + - task: NodeTool@0 + inputs: + versionSource: fromFile + versionFilePath: .nvmrc + + - template: ../cli/cli-apply-patches.yml@self + + - task: Npm@1 + displayName: Download openssl prebuilt + inputs: + command: custom + customCommand: pack @vscode-internal/openssl-prebuilt@0.0.11 + customRegistry: useFeed + customFeed: "Monaco/openssl-prebuilt" + workingDir: $(Build.ArtifactStagingDirectory) + + - powershell: | + mkdir $(Build.ArtifactStagingDirectory)/openssl + tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.11.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl + displayName: Extract openssl prebuilt + + - template: ./steps/product-build-win32-install-rust.yml@self + parameters: + targets: + - ${{ if eq(parameters.VSCODE_ARCH, 'x64') }}: + - x86_64-pc-windows-msvc + - ${{ if eq(parameters.VSCODE_ARCH, 'arm64') }}: + - aarch64-pc-windows-msvc + + - template: ../cli/cli-compile.yml@self + parameters: + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} + ${{ if eq(parameters.VSCODE_ARCH, 'x64') }}: + VSCODE_CLI_TARGET: x86_64-pc-windows-msvc + ${{ if eq(parameters.VSCODE_ARCH, 'arm64') }}: + VSCODE_CLI_TARGET: aarch64-pc-windows-msvc + VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_win32_$(VSCODE_ARCH)_cli + VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }} + VSCODE_CLI_ENV: + OPENSSL_LIB_DIR: $(Build.ArtifactStagingDirectory)/openssl/$(VSCODE_ARCH)-windows-static/lib + OPENSSL_INCLUDE_DIR: $(Build.ArtifactStagingDirectory)/openssl/$(VSCODE_ARCH)-windows-static/include + ${{ if eq(parameters.VSCODE_ARCH, 'x64') }}: + RUSTFLAGS: "-Ctarget-feature=+crt-static -Clink-args=/guard:cf -Clink-args=/CETCOMPAT" + ${{ if eq(parameters.VSCODE_ARCH, 'arm64') }}: + RUSTFLAGS: "-Ctarget-feature=+crt-static -Clink-args=/guard:cf -Clink-args=/CETCOMPAT:NO" + CFLAGS: "/guard:cf /Qspectre" diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index a0b040c52c2..ef2a7193644 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -1,10 +1,10 @@ parameters: - - name: VSCODE_QUALITY - type: string - name: VSCODE_ARCH type: string - name: VSCODE_CIBUILD type: boolean + - name: VSCODE_QUALITY + type: string - name: VSCODE_RUN_ELECTRON_TESTS type: boolean default: false @@ -14,360 +14,90 @@ parameters: - name: VSCODE_RUN_REMOTE_TESTS type: boolean default: false - - name: VSCODE_TEST_ARTIFACT_NAME - type: string - default: "" -steps: - - task: NodeTool@0 - inputs: - versionSource: fromFile - versionFilePath: .nvmrc - - - task: UsePythonVersion@0 - inputs: - versionSpec: "3.x" - addToPath: true - - - template: ../distro/download-distro.yml@self - - - task: AzureKeyVault@2 - displayName: "Azure Key Vault: Get Secrets" - inputs: - azureSubscription: vscode - KeyVaultName: vscode-build-secrets - SecretsFilter: "github-distro-mixin-password" - - - task: DownloadPipelineArtifact@2 - inputs: - artifact: Compilation - path: $(Build.ArtifactStagingDirectory) - displayName: Download compilation output - - - task: ExtractFiles@1 - displayName: Extract compilation output - inputs: - archiveFilePatterns: "$(Build.ArtifactStagingDirectory)/compilation.tar.gz" - cleanDestinationFolder: false - - - powershell: node build/setup-npm-registry.js $env:NPM_REGISTRY - condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Registry - - - pwsh: | - mkdir .build -ea 0 - node build/azure-pipelines/common/computeNodeModulesCacheKey.js win32 $(VSCODE_ARCH) $(node -p process.arch) > .build/packagelockhash - displayName: Prepare node_modules cache key - - - task: Cache@2 - inputs: - key: '"node_modules" | .build/packagelockhash' - path: .build/node_modules_cache - cacheHitVar: NODE_MODULES_RESTORED - displayName: Restore node_modules cache - - - powershell: 7z.exe x .build/node_modules_cache/cache.7z -aoa - condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Extract node_modules cache - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - # Set the private NPM registry to the global npmrc file - # so that authentication works for subfolders like build/, remote/, extensions/ etc - # which does not have their own .npmrc file - exec { npm config set registry "$env:NPM_REGISTRY" } - $NpmrcPath = (npm config get userconfig) - echo "##vso[task.setvariable variable=NPMRC_PATH]$NpmrcPath" - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM - - - task: npmAuthenticate@0 - inputs: - workingFile: $(NPMRC_PATH) - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) - displayName: Setup NPM Authentication - - # Remove once https://github.com/parcel-bundler/watcher/pull/202 is merged. - - pwsh: | - $includes = @' - { - 'target_defaults': { - 'conditions': [ - ['OS=="win"', { - "msvs_settings": { - "VCCLCompilerTool": { - "AdditionalOptions": [ - "/guard:cf", - "/w34244", - "/w34267", - ] - }, - "VCLinkerTool": { - "AdditionalOptions": [ - "/guard:cf", - ] - } - } - }] - ] - } - } - '@ - - if (!(Test-Path "~/.gyp")) { - mkdir "~/.gyp" - } - echo $includes > "~/.gyp/include.gypi" - displayName: Create include.gypi - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npm ci } - env: - npm_config_arch: $(VSCODE_ARCH) - npm_config_foreground_scripts: "true" - ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - GITHUB_TOKEN: "$(github-distro-mixin-password)" - retryCountOnTaskFailure: 5 - displayName: Install dependencies - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - - powershell: node build/azure-pipelines/distro/mixin-npm - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Mixin distro node modules - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt } - exec { mkdir -Force .build/node_modules_cache } - exec { 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt } - condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - displayName: Create node_modules archive - - - powershell: node build/azure-pipelines/distro/mixin-quality - displayName: Mixin distro quality - - - template: ../common/install-builtin-extensions.yml@self - - - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - - powershell: node build\lib\policies win32 - displayName: Generate Group Policy definitions - retryCountOnTaskFailure: 3 - - - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'exploration')) }}: - - powershell: node build/win32/explorer-dll-fetcher .build/win32/appx - displayName: Download Explorer dll - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npm run gulp "vscode-win32-$(VSCODE_ARCH)-min-ci" } - exec { npm run gulp "vscode-win32-$(VSCODE_ARCH)-inno-updater" } - echo "##vso[task.setvariable variable=BUILT_CLIENT]true" - echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build client - - # Note: the appx prepare step has to follow Build client step since build step replaces the template - # strings in the raw manifest file at resources/win32/appx/AppxManifest.xml and places it under - # /appx/manifest, we need a separate step to prepare the appx package with the - # final contents. In our case only the manifest file is bundled into the appx package. - - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'exploration')) }}: - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - # Add Windows SDK to path - $sdk = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64" - $env:PATH = "$sdk;$env:PATH" - $AppxName = if ('$(VSCODE_QUALITY)' -eq 'stable') { 'code' } else { 'code_insider' } - makeappx pack /d "$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)/appx/manifest" /p "$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)/appx/${AppxName}_$(VSCODE_ARCH).appx" /nv - # Remove the raw manifest folder - Remove-Item -Path "$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)/appx/manifest" -Recurse -Force - displayName: Prepare appx package - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npm run gulp "vscode-reh-win32-$(VSCODE_ARCH)-min-ci" } - mv ..\vscode-reh-win32-$(VSCODE_ARCH) ..\vscode-server-win32-$(VSCODE_ARCH) # TODO@joaomoreno - echo "##vso[task.setvariable variable=BUILT_SERVER]true" - echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(CodeSigningFolderPath),$(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build server - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npm run gulp "vscode-reh-web-win32-$(VSCODE_ARCH)-min-ci" } - mv ..\vscode-reh-web-win32-$(VSCODE_ARCH) ..\vscode-server-win32-$(VSCODE_ARCH)-web # TODO@joaomoreno - echo "##vso[task.setvariable variable=BUILT_WEB]true" - env: - GITHUB_TOKEN: "$(github-distro-mixin-password)" - displayName: Build server (web) - - - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - - task: DownloadPipelineArtifact@2 - inputs: - artifact: unsigned_vscode_cli_win32_$(VSCODE_ARCH)_cli - patterns: "**" - path: $(Build.ArtifactStagingDirectory)/cli - displayName: Download VS Code CLI - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $ArtifactName = (gci -Path "$(Build.ArtifactStagingDirectory)/cli" | Select-Object -last 1).FullName - Expand-Archive -Path $ArtifactName -DestinationPath "$(Build.ArtifactStagingDirectory)/cli" - $AppProductJson = Get-Content -Raw -Path "$(Agent.BuildDirectory)\VSCode-win32-$(VSCODE_ARCH)\resources\app\product.json" | ConvertFrom-Json - $CliAppName = $AppProductJson.tunnelApplicationName - $AppName = $AppProductJson.applicationName - Move-Item -Path "$(Build.ArtifactStagingDirectory)/cli/$AppName.exe" -Destination "$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)/bin/$CliAppName.exe" - displayName: Move VS Code CLI - - - task: UseDotNet@2 - inputs: - version: 6.x - - - task: EsrpCodeSigning@5 - inputs: - UseMSIAuthentication: true - ConnectedServiceName: vscode-esrp - AppRegistrationClientId: $(ESRP_CLIENT_ID) - AppRegistrationTenantId: $(ESRP_TENANT_ID) - AuthAKVName: vscode-esrp - AuthSignCertName: esrp-sign - FolderPath: . - Pattern: noop - displayName: 'Install ESRP Tooling' - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $EsrpCodeSigningTool = (gci -directory -filter EsrpCodeSigning_* $(Agent.RootDirectory)\_tasks | Select-Object -last 1).FullName - $Version = (gci -directory $EsrpCodeSigningTool | Select-Object -last 1).FullName - echo "##vso[task.setvariable variable=EsrpCliDllPath]$Version\net6.0\esrpcli.dll" - displayName: Find ESRP CLI - - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - mkdir -Force .build/node-cpuprofile - exec { npx deemon --detach --wait -- npx zx build/azure-pipelines/win32/codesign.js } - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - NODE_DEBUG: "net,child_process" - NODE_OPTIONS: "--report-filename=stdout --report-uncaught-exception --report-on-fatalerror --cpu-prof --cpu-prof-dir=.build/node-cpuprofile" - displayName: ✍️ Codesign - - - ${{ if or(eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true), eq(parameters.VSCODE_RUN_BROWSER_TESTS, true), eq(parameters.VSCODE_RUN_REMOTE_TESTS, true)) }}: - - template: product-build-win32-test.yml@self - parameters: - VSCODE_ARCH: ${{ parameters.VSCODE_ARCH }} - VSCODE_RUN_ELECTRON_TESTS: ${{ parameters.VSCODE_RUN_ELECTRON_TESTS }} - VSCODE_RUN_BROWSER_TESTS: ${{ parameters.VSCODE_RUN_BROWSER_TESTS }} - VSCODE_RUN_REMOTE_TESTS: ${{ parameters.VSCODE_RUN_REMOTE_TESTS }} - VSCODE_TEST_ARTIFACT_NAME: ${{ parameters.VSCODE_TEST_ARTIFACT_NAME }} - - - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { npx deemon --attach -- npx zx build/azure-pipelines/win32/codesign.js } - condition: succeededOrFailed() - env: - NODE_DEBUG: "net,child_process" - NODE_OPTIONS: "--report-filename=stdout --report-uncaught-exception --report-on-fatalerror --cpu-prof --cpu-prof-dir=.build/node-cpuprofile" - displayName: "✍️ Post-job: Codesign" - - - powershell: | - $ErrorActionPreference = "Stop" - - $PackageJson = Get-Content -Raw -Path ..\VSCode-win32-$(VSCODE_ARCH)\resources\app\package.json | ConvertFrom-Json - $Version = $PackageJson.version - - $ClientArchivePath = ".build\win32-$(VSCODE_ARCH)\VSCode-win32-$(VSCODE_ARCH)-$Version.zip" - $ServerArchivePath = ".build\win32-$(VSCODE_ARCH)\vscode-server-win32-$(VSCODE_ARCH).zip" - $WebArchivePath = ".build\win32-$(VSCODE_ARCH)\vscode-server-win32-$(VSCODE_ARCH)-web.zip" - - $SystemSetupPath = ".build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup-$(VSCODE_ARCH)-$Version.exe" - $UserSetupPath = ".build\win32-$(VSCODE_ARCH)\user-setup\VSCodeUserSetup-$(VSCODE_ARCH)-$Version.exe" - - mv .build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup.exe $SystemSetupPath - mv .build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe $UserSetupPath - - echo "##vso[task.setvariable variable=CLIENT_PATH]$ClientArchivePath" - echo "##vso[task.setvariable variable=SERVER_PATH]$ServerArchivePath" - echo "##vso[task.setvariable variable=WEB_PATH]$WebArchivePath" - - echo "##vso[task.setvariable variable=SYSTEM_SETUP_PATH]$SystemSetupPath" - echo "##vso[task.setvariable variable=USER_SETUP_PATH]$UserSetupPath" - condition: succeededOrFailed() - displayName: Move setup packages - - - powershell: echo "##vso[task.setvariable variable=ARTIFACT_PREFIX]attempt$(System.JobAttempt)_" - condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) - displayName: Generate artifact prefix - - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: .build/node-cpuprofile - artifactName: $(ARTIFACT_PREFIX)node-cpuprofile-$(VSCODE_ARCH) - displayName: Publish Codesign cpu profile - sbomEnabled: false - - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: $(CLIENT_PATH) - artifactName: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_archive - displayName: Publish archive - sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH) - sbomPackageName: "VS Code Windows $(VSCODE_ARCH)" - sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['CLIENT_PATH'], '')) - - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: $(SERVER_PATH) - artifactName: $(ARTIFACT_PREFIX)vscode_server_win32_$(VSCODE_ARCH)_archive - displayName: Publish server archive - sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH) - sbomPackageName: "VS Code Windows $(VSCODE_ARCH) Server" - sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], '')) - - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: $(WEB_PATH) - artifactName: $(ARTIFACT_PREFIX)vscode_web_win32_$(VSCODE_ARCH)_archive - displayName: Publish web server archive - sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)-web - sbomPackageName: "VS Code Windows $(VSCODE_ARCH) Web" - sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], '')) - - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: $(SYSTEM_SETUP_PATH) - artifactName: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_setup - displayName: Publish system setup - sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH) - sbomPackageName: "VS Code Windows $(VSCODE_ARCH) System Setup" - sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['SYSTEM_SETUP_PATH'], '')) - - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: $(USER_SETUP_PATH) - artifactName: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_user-setup - displayName: Publish user setup - sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH) - sbomPackageName: "VS Code Windows $(VSCODE_ARCH) User Setup" - sbomPackageVersion: $(Build.SourceVersion) - condition: and(succeededOrFailed(), ne(variables['USER_SETUP_PATH'], '')) +jobs: + - job: Windows_${{ parameters.VSCODE_ARCH }} + displayName: Windows${{ upper(parameters.VSCODE_ARCH) }} + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: ${{ parameters.VSCODE_ARCH }} + templateContext: + outputParentDirectory: $(Build.SourcesDirectory)/.build/win32-$(VSCODE_ARCH) + outputs: + - ${{ if or(eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true), eq(parameters.VSCODE_RUN_BROWSER_TESTS, true), eq(parameters.VSCODE_RUN_REMOTE_TESTS, true)) }}: + - output: pipelineArtifact + targetPath: .build/crashes + artifactName: crash-dump-windows-$(VSCODE_ARCH)-$(System.JobAttempt) + displayName: Publish Crash Reports + sbomEnabled: false + isProduction: false + condition: failed() + - output: pipelineArtifact + targetPath: node_modules + artifactName: node-modules-windows-$(VSCODE_ARCH)-$(System.JobAttempt) + displayName: Publish Node Modules + sbomEnabled: false + isProduction: false + condition: failed() + - output: pipelineArtifact + targetPath: .build/logs + artifactName: logs-windows-$(VSCODE_ARCH)-$(System.JobAttempt) + displayName: Publish Log Files + sbomEnabled: false + isProduction: false + condition: succeededOrFailed() + - output: pipelineArtifact + targetPath: .build/node-cpuprofile + artifactName: $(ARTIFACT_PREFIX)node-cpuprofile-$(VSCODE_ARCH) + displayName: Publish Codesign cpu profile + sbomEnabled: false + isProduction: false + - output: pipelineArtifact + targetPath: $(SYSTEM_SETUP_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_setup + displayName: Publish system setup + sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH) + sbomPackageName: "VS Code Windows $(VSCODE_ARCH) System Setup" + sbomPackageVersion: $(Build.SourceVersion) + condition: and(succeededOrFailed(), ne(variables['SYSTEM_SETUP_PATH'], '')) + - output: pipelineArtifact + targetPath: $(USER_SETUP_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_user-setup + displayName: Publish user setup + sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH) + sbomPackageName: "VS Code Windows $(VSCODE_ARCH) User Setup" + sbomPackageVersion: $(Build.SourceVersion) + condition: and(succeededOrFailed(), ne(variables['USER_SETUP_PATH'], '')) + - output: pipelineArtifact + targetPath: $(CLIENT_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_client_win32_$(VSCODE_ARCH)_archive + displayName: Publish archive + sbomBuildDropPath: $(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH) + sbomPackageName: "VS Code Windows $(VSCODE_ARCH)" + sbomPackageVersion: $(Build.SourceVersion) + condition: and(succeededOrFailed(), ne(variables['CLIENT_PATH'], '')) + - output: pipelineArtifact + targetPath: $(SERVER_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_server_win32_$(VSCODE_ARCH)_archive + displayName: Publish server archive + sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH) + sbomPackageName: "VS Code Windows $(VSCODE_ARCH) Server" + sbomPackageVersion: $(Build.SourceVersion) + condition: and(succeededOrFailed(), ne(variables['SERVER_PATH'], '')) + - output: pipelineArtifact + targetPath: $(WEB_PATH) + artifactName: $(ARTIFACT_PREFIX)vscode_web_win32_$(VSCODE_ARCH)_archive + displayName: Publish web server archive + sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)-web + sbomPackageName: "VS Code Windows $(VSCODE_ARCH) Web" + sbomPackageVersion: $(Build.SourceVersion) + condition: and(succeededOrFailed(), ne(variables['WEB_PATH'], '')) + steps: + - template: ./steps/product-build-win32-compile.yml@self + parameters: + VSCODE_ARCH: ${{ parameters.VSCODE_ARCH }} + VSCODE_CIBUILD: ${{ parameters.VSCODE_CIBUILD }} + VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} + VSCODE_RUN_ELECTRON_TESTS: ${{ parameters.VSCODE_RUN_ELECTRON_TESTS }} + VSCODE_RUN_BROWSER_TESTS: ${{ parameters.VSCODE_RUN_BROWSER_TESTS }} + VSCODE_RUN_REMOTE_TESTS: ${{ parameters.VSCODE_RUN_REMOTE_TESTS }} diff --git a/build/azure-pipelines/win32/sdl-scan-win32.yml b/build/azure-pipelines/win32/sdl-scan-win32.yml index ba60b881392..dba656eff53 100644 --- a/build/azure-pipelines/win32/sdl-scan-win32.yml +++ b/build/azure-pipelines/win32/sdl-scan-win32.yml @@ -5,6 +5,8 @@ parameters: type: string steps: + - template: ../common/checkout.yml@self + - task: NodeTool@0 inputs: versionSource: fromFile diff --git a/build/azure-pipelines/cli/cli-win32-sign.yml b/build/azure-pipelines/win32/steps/product-build-win32-cli-sign.yml similarity index 68% rename from build/azure-pipelines/cli/cli-win32-sign.yml rename to build/azure-pipelines/win32/steps/product-build-win32-cli-sign.yml index 3c972ae0282..15d224d1729 100644 --- a/build/azure-pipelines/cli/cli-win32-sign.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-cli-sign.yml @@ -33,15 +33,15 @@ steps: displayName: Download artifact inputs: artifact: ${{ target }} - path: $(Build.ArtifactStagingDirectory)/pkg/${{ target }} + path: $(Build.BinariesDirectory)/pkg/${{ target }} - task: ExtractFiles@1 displayName: Extract artifact inputs: - archiveFilePatterns: $(Build.ArtifactStagingDirectory)/pkg/${{ target }}/*.zip - destinationFolder: $(Build.ArtifactStagingDirectory)/sign/${{ target }} + archiveFilePatterns: $(Build.BinariesDirectory)/pkg/${{ target }}/*.zip + destinationFolder: $(Build.BinariesDirectory)/sign/${{ target }} - - powershell: node build\azure-pipelines\common\sign $env:EsrpCliDllPath sign-windows $(Build.ArtifactStagingDirectory)/sign "*.exe" + - powershell: node build\azure-pipelines\common\sign $env:EsrpCliDllPath sign-windows $(Build.BinariesDirectory)/sign "*.exe" env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: ✍️ Codesign @@ -55,16 +55,7 @@ steps: - task: ArchiveFiles@2 displayName: Archive signed files inputs: - rootFolderOrFile: $(Build.ArtifactStagingDirectory)/sign/${{ target }} + rootFolderOrFile: $(Build.BinariesDirectory)/sign/${{ target }} includeRootFolder: false archiveType: zip archiveFile: $(Build.ArtifactStagingDirectory)/$(ASSET_ID).zip - - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: $(Build.ArtifactStagingDirectory)/$(ASSET_ID).zip - artifactName: $(ASSET_ID) - displayName: Publish signed artifact with ID $(ASSET_ID) - sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/sign/${{ target }} - sbomPackageName: "VS Code Windows ${{ target }} CLI" - sbomPackageVersion: $(Build.SourceVersion) diff --git a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml new file mode 100644 index 00000000000..0a2ba320dfb --- /dev/null +++ b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml @@ -0,0 +1,314 @@ +parameters: + - name: VSCODE_ARCH + type: string + - name: VSCODE_CIBUILD + type: boolean + - name: VSCODE_QUALITY + type: string + - name: VSCODE_RUN_ELECTRON_TESTS + type: boolean + default: false + - name: VSCODE_RUN_BROWSER_TESTS + type: boolean + default: false + - name: VSCODE_RUN_REMOTE_TESTS + type: boolean + default: false + +steps: + - template: ../../common/checkout.yml@self + + - task: NodeTool@0 + inputs: + versionSource: fromFile + versionFilePath: .nvmrc + + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.x" + addToPath: true + + - template: ../../distro/download-distro.yml@self + + - task: AzureKeyVault@2 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: vscode + KeyVaultName: vscode-build-secrets + SecretsFilter: "github-distro-mixin-password" + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: Compilation + path: $(Build.ArtifactStagingDirectory) + displayName: Download compilation output + + - task: ExtractFiles@1 + displayName: Extract compilation output + inputs: + archiveFilePatterns: "$(Build.ArtifactStagingDirectory)/compilation.tar.gz" + cleanDestinationFolder: false + + - powershell: node build/setup-npm-registry.js $env:NPM_REGISTRY + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Registry + + - pwsh: | + mkdir .build -ea 0 + node build/azure-pipelines/common/computeNodeModulesCacheKey.js win32 $(VSCODE_ARCH) $(node -p process.arch) > .build/packagelockhash + displayName: Prepare node_modules cache key + + - task: Cache@2 + inputs: + key: '"node_modules" | .build/packagelockhash' + path: .build/node_modules_cache + cacheHitVar: NODE_MODULES_RESTORED + displayName: Restore node_modules cache + + - powershell: 7z.exe x .build/node_modules_cache/cache.7z -aoa + condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Extract node_modules cache + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + # Set the private NPM registry to the global npmrc file + # so that authentication works for subfolders like build/, remote/, extensions/ etc + # which does not have their own .npmrc file + exec { npm config set registry "$env:NPM_REGISTRY" } + $NpmrcPath = (npm config get userconfig) + echo "##vso[task.setvariable variable=NPMRC_PATH]$NpmrcPath" + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM + + - task: npmAuthenticate@0 + inputs: + workingFile: $(NPMRC_PATH) + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), ne(variables['NPM_REGISTRY'], 'none')) + displayName: Setup NPM Authentication + + # Remove once https://github.com/parcel-bundler/watcher/pull/202 is merged. + - pwsh: | + $includes = @' + { + 'target_defaults': { + 'conditions': [ + ['OS=="win"', { + "msvs_settings": { + "VCCLCompilerTool": { + "AdditionalOptions": [ + "/guard:cf", + "/w34244", + "/w34267", + ] + }, + "VCLinkerTool": { + "AdditionalOptions": [ + "/guard:cf", + ] + } + } + }] + ] + } + } + '@ + + if (!(Test-Path "~/.gyp")) { + mkdir "~/.gyp" + } + echo $includes > "~/.gyp/include.gypi" + displayName: Create include.gypi + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm ci } + env: + npm_config_arch: $(VSCODE_ARCH) + npm_config_foreground_scripts: "true" + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + retryCountOnTaskFailure: 5 + displayName: Install dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + + - powershell: node build/azure-pipelines/distro/mixin-npm + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Mixin distro node modules + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt } + exec { mkdir -Force .build/node_modules_cache } + exec { 7z.exe a .build/node_modules_cache/cache.7z -mx3 `@.build/node_modules_list.txt } + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Create node_modules archive + + - powershell: node build/azure-pipelines/distro/mixin-quality + displayName: Mixin distro quality + + - template: ../../common/install-builtin-extensions.yml@self + + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - powershell: node build\lib\policies win32 + displayName: Generate Group Policy definitions + retryCountOnTaskFailure: 3 + + - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'exploration')) }}: + - powershell: node build/win32/explorer-dll-fetcher .build/win32/appx + displayName: Download Explorer dll + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm run gulp "vscode-win32-$(VSCODE_ARCH)-min-ci" } + exec { npm run gulp "vscode-win32-$(VSCODE_ARCH)-inno-updater" } + echo "##vso[task.setvariable variable=BUILT_CLIENT]true" + echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)" + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Build client + + # Note: the appx prepare step has to follow Build client step since build step replaces the template + # strings in the raw manifest file at resources/win32/appx/AppxManifest.xml and places it under + # /appx/manifest, we need a separate step to prepare the appx package with the + # final contents. In our case only the manifest file is bundled into the appx package. + - ${{ if and(ne(parameters.VSCODE_CIBUILD, true), ne(parameters.VSCODE_QUALITY, 'exploration')) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + # Add Windows SDK to path + $sdk = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64" + $env:PATH = "$sdk;$env:PATH" + $AppxName = if ('$(VSCODE_QUALITY)' -eq 'stable') { 'code' } else { 'code_insider' } + makeappx pack /d "$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)/appx/manifest" /p "$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)/appx/${AppxName}_$(VSCODE_ARCH).appx" /nv + # Remove the raw manifest folder + Remove-Item -Path "$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)/appx/manifest" -Recurse -Force + displayName: Prepare appx package + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm run gulp "vscode-reh-win32-$(VSCODE_ARCH)-min-ci" } + mv ..\vscode-reh-win32-$(VSCODE_ARCH) ..\vscode-server-win32-$(VSCODE_ARCH) # TODO@joaomoreno + echo "##vso[task.setvariable variable=BUILT_SERVER]true" + echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(CodeSigningFolderPath),$(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)" + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Build server + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npm run gulp "vscode-reh-web-win32-$(VSCODE_ARCH)-min-ci" } + mv ..\vscode-reh-web-win32-$(VSCODE_ARCH) ..\vscode-server-win32-$(VSCODE_ARCH)-web # TODO@joaomoreno + echo "##vso[task.setvariable variable=BUILT_WEB]true" + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Build server (web) + + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - task: DownloadPipelineArtifact@2 + inputs: + artifact: unsigned_vscode_cli_win32_$(VSCODE_ARCH)_cli + patterns: "**" + path: $(Build.ArtifactStagingDirectory)/cli + displayName: Download VS Code CLI + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $ArtifactName = (gci -Path "$(Build.ArtifactStagingDirectory)/cli" | Select-Object -last 1).FullName + Expand-Archive -Path $ArtifactName -DestinationPath "$(Build.ArtifactStagingDirectory)/cli" + $AppProductJson = Get-Content -Raw -Path "$(Agent.BuildDirectory)\VSCode-win32-$(VSCODE_ARCH)\resources\app\product.json" | ConvertFrom-Json + $CliAppName = $AppProductJson.tunnelApplicationName + $AppName = $AppProductJson.applicationName + Move-Item -Path "$(Build.ArtifactStagingDirectory)/cli/$AppName.exe" -Destination "$(Agent.BuildDirectory)/VSCode-win32-$(VSCODE_ARCH)/bin/$CliAppName.exe" + displayName: Move VS Code CLI + + - task: UseDotNet@2 + inputs: + version: 6.x + + - task: EsrpCodeSigning@5 + inputs: + UseMSIAuthentication: true + ConnectedServiceName: vscode-esrp + AppRegistrationClientId: $(ESRP_CLIENT_ID) + AppRegistrationTenantId: $(ESRP_TENANT_ID) + AuthAKVName: vscode-esrp + AuthSignCertName: esrp-sign + FolderPath: . + Pattern: noop + displayName: 'Install ESRP Tooling' + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $EsrpCodeSigningTool = (gci -directory -filter EsrpCodeSigning_* $(Agent.RootDirectory)\_tasks | Select-Object -last 1).FullName + $Version = (gci -directory $EsrpCodeSigningTool | Select-Object -last 1).FullName + echo "##vso[task.setvariable variable=EsrpCliDllPath]$Version\net6.0\esrpcli.dll" + displayName: Find ESRP CLI + + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + mkdir -Force .build/node-cpuprofile + exec { npx deemon --detach --wait -- npx zx build/azure-pipelines/win32/codesign.js } + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + NODE_DEBUG: "net,child_process" + NODE_OPTIONS: "--report-filename=stdout --report-uncaught-exception --report-on-fatalerror --cpu-prof --cpu-prof-dir=.build/node-cpuprofile" + displayName: ✍️ Codesign + + - ${{ if or(eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true), eq(parameters.VSCODE_RUN_BROWSER_TESTS, true), eq(parameters.VSCODE_RUN_REMOTE_TESTS, true)) }}: + - template: product-build-win32-test.yml@self + parameters: + VSCODE_ARCH: ${{ parameters.VSCODE_ARCH }} + VSCODE_RUN_ELECTRON_TESTS: ${{ parameters.VSCODE_RUN_ELECTRON_TESTS }} + VSCODE_RUN_BROWSER_TESTS: ${{ parameters.VSCODE_RUN_BROWSER_TESTS }} + VSCODE_RUN_REMOTE_TESTS: ${{ parameters.VSCODE_RUN_REMOTE_TESTS }} + + - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { npx deemon --attach -- npx zx build/azure-pipelines/win32/codesign.js } + condition: succeededOrFailed() + env: + NODE_DEBUG: "net,child_process" + NODE_OPTIONS: "--report-filename=stdout --report-uncaught-exception --report-on-fatalerror --cpu-prof --cpu-prof-dir=.build/node-cpuprofile" + displayName: "✍️ Post-job: Codesign" + + - powershell: | + $ErrorActionPreference = "Stop" + + $PackageJson = Get-Content -Raw -Path ..\VSCode-win32-$(VSCODE_ARCH)\resources\app\package.json | ConvertFrom-Json + $Version = $PackageJson.version + + $ClientArchivePath = ".build\win32-$(VSCODE_ARCH)\VSCode-win32-$(VSCODE_ARCH)-$Version.zip" + $ServerArchivePath = ".build\win32-$(VSCODE_ARCH)\vscode-server-win32-$(VSCODE_ARCH).zip" + $WebArchivePath = ".build\win32-$(VSCODE_ARCH)\vscode-server-win32-$(VSCODE_ARCH)-web.zip" + + $SystemSetupPath = ".build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup-$(VSCODE_ARCH)-$Version.exe" + $UserSetupPath = ".build\win32-$(VSCODE_ARCH)\user-setup\VSCodeUserSetup-$(VSCODE_ARCH)-$Version.exe" + + mv .build\win32-$(VSCODE_ARCH)\system-setup\VSCodeSetup.exe $SystemSetupPath + mv .build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe $UserSetupPath + + echo "##vso[task.setvariable variable=CLIENT_PATH]$ClientArchivePath" + echo "##vso[task.setvariable variable=SERVER_PATH]$ServerArchivePath" + echo "##vso[task.setvariable variable=WEB_PATH]$WebArchivePath" + + echo "##vso[task.setvariable variable=SYSTEM_SETUP_PATH]$SystemSetupPath" + echo "##vso[task.setvariable variable=USER_SETUP_PATH]$UserSetupPath" + condition: succeededOrFailed() + displayName: Move setup packages + + - powershell: echo "##vso[task.setvariable variable=ARTIFACT_PREFIX]attempt$(System.JobAttempt)_" + condition: and(succeededOrFailed(), notIn(variables['Agent.JobStatus'], 'Succeeded', 'SucceededWithIssues')) + displayName: Generate artifact prefix diff --git a/build/azure-pipelines/cli/install-rust-win32.yml b/build/azure-pipelines/win32/steps/product-build-win32-install-rust.yml similarity index 99% rename from build/azure-pipelines/cli/install-rust-win32.yml rename to build/azure-pipelines/win32/steps/product-build-win32-install-rust.yml index bff114fccd0..a9c3b7e6432 100644 --- a/build/azure-pipelines/cli/install-rust-win32.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-install-rust.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.85 + default: 1.88 - name: targets default: [] type: object diff --git a/build/azure-pipelines/win32/product-build-win32-test.yml b/build/azure-pipelines/win32/steps/product-build-win32-test.yml similarity index 79% rename from build/azure-pipelines/win32/product-build-win32-test.yml rename to build/azure-pipelines/win32/steps/product-build-win32-test.yml index 7d5222e347f..034cbb8f44b 100644 --- a/build/azure-pipelines/win32/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-test.yml @@ -7,8 +7,6 @@ parameters: type: boolean - name: VSCODE_RUN_REMOTE_TESTS type: boolean - - name: VSCODE_TEST_ARTIFACT_NAME - type: string steps: - powershell: npm exec -- npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" @@ -142,47 +140,6 @@ steps: continueOnError: true condition: succeededOrFailed() - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: .build\crashes - ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: - artifactName: crash-dump-windows-$(VSCODE_ARCH)-$(System.JobAttempt) - ${{ else }}: - artifactName: crash-dump-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) - isProduction: false - sbomEnabled: false - displayName: "Publish Crash Reports" - continueOnError: true - condition: failed() - - # In order to properly symbolify above crash reports - # (if any), we need the compiled native modules too - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: node_modules - ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: - artifactName: node-modules-windows-$(VSCODE_ARCH)-$(System.JobAttempt) - ${{ else }}: - artifactName: node-modules-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) - isProduction: false - sbomEnabled: false - displayName: "Publish Node Modules" - continueOnError: true - condition: failed() - - - template: ../common/publish-artifact.yml@self - parameters: - targetPath: .build\logs - ${{ if eq(parameters.VSCODE_TEST_ARTIFACT_NAME, '') }}: - artifactName: logs-windows-$(VSCODE_ARCH)-$(System.JobAttempt) - ${{ else }}: - artifactName: logs-windows-$(VSCODE_ARCH)-${{ parameters.VSCODE_TEST_ARTIFACT_NAME }}-$(System.JobAttempt) - isProduction: false - sbomEnabled: false - displayName: "Publish Log Files" - continueOnError: true - condition: succeededOrFailed() - - task: PublishTestResults@2 displayName: Publish Tests Results inputs: diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 687bd16fd8c..7826f48490b 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -51,6 +51,7 @@ const compilations = [ 'extensions/markdown-math/tsconfig.json', 'extensions/media-preview/tsconfig.json', 'extensions/merge-conflict/tsconfig.json', + 'extensions/mermaid-chat-features/tsconfig.json', 'extensions/terminal-suggest/tsconfig.json', 'extensions/microsoft-authentication/tsconfig.json', 'extensions/notebook-renderers/tsconfig.json', diff --git a/build/lib/extensions.js b/build/lib/extensions.js index aeea722d372..c80a1be1a84 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -510,11 +510,12 @@ function translatePackageJSON(packageJSON, packageNLSPath) { const extensionsPath = path_1.default.join(root, 'extensions'); // Additional projects to run esbuild on. These typically build code for webviews const esbuildMediaScripts = [ + 'ipynb/esbuild.mjs', 'markdown-language-features/esbuild-notebook.mjs', 'markdown-language-features/esbuild-preview.mjs', 'markdown-math/esbuild.mjs', + 'mermaid-chat-features/esbuild-chat-webview.mjs', 'notebook-renderers/esbuild.mjs', - 'ipynb/esbuild.mjs', 'simple-browser/esbuild-preview.mjs', ]; async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index b997fe4046e..9e7cf9f954a 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -559,11 +559,12 @@ const extensionsPath = path.join(root, 'extensions'); // Additional projects to run esbuild on. These typically build code for webviews const esbuildMediaScripts = [ + 'ipynb/esbuild.mjs', 'markdown-language-features/esbuild-notebook.mjs', 'markdown-language-features/esbuild-preview.mjs', 'markdown-math/esbuild.mjs', + 'mermaid-chat-features/esbuild-chat-webview.mjs', 'notebook-renderers/esbuild.mjs', - 'ipynb/esbuild.mjs', 'simple-browser/esbuild-preview.mjs', ]; diff --git a/build/npm/dirs.js b/build/npm/dirs.js index a7ee7c3a6c7..33c18196dd4 100644 --- a/build/npm/dirs.js +++ b/build/npm/dirs.js @@ -33,6 +33,7 @@ const dirs = [ 'extensions/markdown-math', 'extensions/media-preview', 'extensions/merge-conflict', + 'extensions/mermaid-chat-features', 'extensions/microsoft-authentication', 'extensions/notebook-renderers', 'extensions/npm', diff --git a/build/npm/mixin-telemetry-docs.mjs b/build/npm/mixin-telemetry-docs.mjs new file mode 100644 index 00000000000..fe8a6aec446 --- /dev/null +++ b/build/npm/mixin-telemetry-docs.mjs @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { execSync } from 'child_process'; +import { join, resolve } from 'path'; +import { existsSync, rmSync } from 'fs'; +import { fileURLToPath } from 'url'; + +const rootPath = resolve(fileURLToPath(import.meta.url), '..', '..', '..'); +const telemetryDocsPath = join(rootPath, 'vscode-telemetry-docs'); +const repoUrl = 'https://github.com/microsoft/vscode-telemetry-docs'; + +console.log('Cloning vscode-telemetry-docs repository...'); + +// Remove existing directory if it exists +if (existsSync(telemetryDocsPath)) { + console.log('Removing existing vscode-telemetry-docs directory...'); + rmSync(telemetryDocsPath, { recursive: true, force: true }); +} + +try { + // Clone the repository (shallow clone of main branch only) + console.log(`Cloning ${repoUrl} to ${telemetryDocsPath}...`); + execSync(`git clone --depth 1 --branch main --single-branch ${repoUrl} vscode-telemetry-docs`, { + cwd: rootPath, + stdio: 'inherit' + }); + + console.log('Successfully cloned vscode-telemetry-docs repository.'); +} catch (error) { + console.error('Failed to clone vscode-telemetry-docs repository:', error.message); + process.exit(1); +} diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 1033e4ecf68..fa8da7d08c6 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -56,7 +56,16 @@ function npmInstall(dir, opts) { if (process.env['npm_config_arch'] === 'arm64') { run('sudo', ['docker', 'run', '--rm', '--privileged', 'multiarch/qemu-user-static', '--reset', '-p', 'yes'], opts); } - run('sudo', ['docker', 'run', '-e', 'GITHUB_TOKEN', '-v', `${process.env['VSCODE_HOST_MOUNT']}:/root/vscode`, '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/root/.netrc`, '-w', path.resolve('/root/vscode', dir), process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], 'sh', '-c', `\"chown -R root:root ${path.resolve('/root/vscode', dir)} && npm i -g node-gyp-build && npm ci\"`], opts); + run('sudo', [ + 'docker', 'run', + '-e', 'GITHUB_TOKEN', + '-v', `${process.env['VSCODE_HOST_MOUNT']}:/root/vscode`, + '-v', `${process.env['VSCODE_HOST_MOUNT']}/.build/.netrc:/root/.netrc`, + '-v', `${process.env['VSCODE_NPMRC_PATH']}:/root/.npmrc`, + '-w', path.resolve('/root/vscode', dir), + process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'], + 'sh', '-c', `\"chown -R root:root ${path.resolve('/root/vscode', dir)} && export PATH="/root/vscode/.build/nodejs-musl/usr/local/bin:$PATH" && npm i -g node-gyp-build && npm ci\"` + ], opts); run('sudo', ['chown', '-R', `${userinfo.uid}:${userinfo.gid}`, `${path.resolve(root, dir)}`], opts); } else { log(dir, 'Installing dependencies...'); diff --git a/build/package-lock.json b/build/package-lock.json index 663a0ea6f13..c86e8700a36 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -49,7 +49,7 @@ "@types/xml2js": "0.0.33", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/ripgrep": "^1.15.13", - "@vscode/vsce": "2.20.1", + "@vscode/vsce": "3.6.1", "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", @@ -73,6 +73,23 @@ "vscode-gulp-watch": "^5.0.3" } }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, "node_modules/@azure/abort-controller": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.2.tgz", @@ -446,6 +463,31 @@ "node": ">=18.0.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@electron/asar": { "version": "3.2.10", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.10.tgz", @@ -966,6 +1008,72 @@ "node": ">=18" } }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", @@ -988,6 +1096,218 @@ "node": ">= 12.13.0" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-sarif-builder": "^3.2.0" + } + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -1000,6 +1320,19 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -1012,6 +1345,166 @@ "node": ">=10" } }, + "node_modules/@textlint/ast-node-types": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.2.2.tgz", + "integrity": "sha512-9ByYNzWV8tpz6BFaRzeRzIov8dkbSZu9q7IWqEIfmRuLWb2qbI/5gTvKcoWT1HYs4XM7IZ8TKSXcuPvMb6eorA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.2.2.tgz", + "integrity": "sha512-oMVaMJ3exFvXhCj3AqmCbLaeYrTNLqaJnLJMIlmnRM3/kZdxvku4OYdaDzgtlI194cVxamOY5AbHBBVnY79kEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.2.2", + "@textlint/resolver": "15.2.2", + "@textlint/types": "15.2.2", + "chalk": "^4.1.2", + "debug": "^4.4.1", + "js-yaml": "^3.14.1", + "lodash": "^4.17.21", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/module-interop": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.2.2.tgz", + "integrity": "sha512-2rmNcWrcqhuR84Iio1WRzlc4tEoOMHd6T7urjtKNNefpTt1owrTJ9WuOe60yD3FrTW0J/R0ux5wxUbP/eaeFOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.2.2.tgz", + "integrity": "sha512-4hGWjmHt0y+5NAkoYZ8FvEkj8Mez9TqfbTm3BPjoV32cIfEixl2poTOgapn1rfm73905GSO3P1jiWjmgvii13Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.2.2", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.2.2.tgz", + "integrity": "sha512-X2BHGAR3yXJsCAjwYEDBIk9qUDWcH4pW61ISfmtejau+tVqKtnbbvEZnMTb6mWgKU1BvTmftd5DmB1XVDUtY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.2.2" + } + }, "node_modules/@types/ansi-colors": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@types/ansi-colors/-/ansi-colors-3.2.0.tgz", @@ -1297,6 +1790,13 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/p-all": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/p-all/-/p-all-1.0.0.tgz", @@ -1332,6 +1832,13 @@ "@types/node": "*" } }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/through": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.29.tgz", @@ -1435,26 +1942,36 @@ } }, "node_modules/@vscode/vsce": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.20.1.tgz", - "integrity": "sha512-ilbvoqvR/1/zseRPBAzYR6aKqSJ+jvda4/BqIwOqTxajpvLtEpK3kMLs77+dJdrlygS+VrP7Yhad8j0ukyD96g==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.6.1.tgz", + "integrity": "sha512-UXtMgeCBl/t5zjn1TX1v1sl5L/oIv3Xc3pkKPGzaqeFCIkp5+wfFFDBXTWDt3d5uUulHnZKORHkMIsKNe9+k5A==", "dev": true, + "license": "MIT", "dependencies": { - "azure-devops-node-api": "^11.0.1", - "chalk": "^2.4.2", + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", "cheerio": "^1.0.0-rc.9", - "commander": "^6.1.0", - "glob": "^7.0.6", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^11.0.0", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", - "markdown-it": "^12.3.2", + "markdown-it": "^14.1.0", "mime": "^1.3.4", "minimatch": "^3.0.3", "parse-semver": "^1.1.1", "read": "^1.0.7", + "secretlint": "^10.1.2", "semver": "^7.5.2", - "tmp": "^0.2.1", + "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", @@ -1465,19 +1982,258 @@ "vsce": "vsce" }, "engines": { - "node": ">= 14" + "node": ">= 20" }, "optionalDependencies": { "keytar": "^7.7.0" } }, - "node_modules/@vscode/vsce/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "node_modules/@vscode/vsce-sign": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.7.tgz", + "integrity": "sha512-cz0GFW8qCxpypOy3y509u26K1FIPMlDIHBwGmDyvEbgoma2v3y5YIHHuijr8zCYBp9kzCCOJd28s/0PG7cA7ew==", "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">= 6" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vscode/vsce/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@vscode/vsce/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@vscode/vsce/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/vsce/node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/glob/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vscode/vsce/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/@vscode/vsce/node_modules/jsonc-parser": { @@ -1498,6 +2254,19 @@ "node": ">=10" } }, + "node_modules/@vscode/vsce/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@vscode/vsce/node_modules/yazl": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", @@ -1530,6 +2299,23 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-colors": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", @@ -1540,6 +2326,22 @@ "node": ">=6" } }, + "node_modules/ansi-escapes": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", + "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", @@ -1552,6 +2354,19 @@ "node": ">=0.10.0" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -1587,10 +2402,21 @@ } }, "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/argparse/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/arr-diff": { "version": "4.0.0", @@ -1619,6 +2445,16 @@ "node": ">=0.10.0" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/async-done": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", @@ -1634,11 +2470,19 @@ "node": ">= 0.10" } }, - "node_modules/azure-devops-node-api": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", - "integrity": "sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==", + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, + "license": "MIT" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", "dependencies": { "tunnel": "0.0.6", "typed-rest-client": "^1.8.4" @@ -1679,6 +2523,22 @@ "node": ">=8" } }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -1704,6 +2564,13 @@ "dev": true, "optional": true }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1803,17 +2670,29 @@ "node": ">=8" } }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, + "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -1961,6 +2840,16 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -1985,6 +2874,29 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2041,12 +2953,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2103,23 +3016,6 @@ "node": ">=10" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -2142,6 +3038,16 @@ "node": ">= 0.4" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", @@ -2224,6 +3130,21 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexify": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", @@ -2236,6 +3157,13 @@ "stream-shift": "^1.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2245,6 +3173,30 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -2275,14 +3227,25 @@ "node": ">=6" } }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" + "license": "MIT", + "engines": { + "node": ">=18" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2292,6 +3255,36 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, "engines": { "node": ">= 0.4" } @@ -2353,6 +3346,20 @@ "node": ">=0.8.0" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/events": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", @@ -2420,12 +3427,53 @@ "node": ">= 0.10" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-parser": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", @@ -2449,6 +3497,16 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -2497,12 +3555,46 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fork-stream": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/fork-stream/-/fork-stream-0.0.4.tgz", "integrity": "sha512-Pqq5NnT78ehvUnAk/We/Jr22vSvanRlFTpAmQ88xBY/M1TlHe+P0ILuEyXS595ysdGfaj22634LBkGMA2GTcpA==", "dev": true }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -2548,21 +3640,28 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2571,6 +3670,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -2676,13 +3789,35 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3" + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2776,23 +3911,12 @@ "node": ">=4" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2800,11 +3924,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -2817,6 +3945,7 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -2923,6 +4052,29 @@ ], "optional": true }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/index-to-position": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", + "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2995,6 +4147,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3067,6 +4229,61 @@ "node": ">=0.10.0" } }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsbi": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.4.tgz", @@ -3079,6 +4296,13 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -3218,12 +4442,13 @@ } }, "node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dev": true, + "license": "MIT", "dependencies": { - "uc.micro": "^1.0.1" + "uc.micro": "^2.0.0" } }, "node_modules/lodash": { @@ -3238,6 +4463,13 @@ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", "dev": true }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -3260,29 +4492,29 @@ } }, "node_modules/markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" }, "bin": { - "markdown-it": "bin/markdown-it.js" + "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/markdown-it/node_modules/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } + "license": "Python-2.0" }, "node_modules/matcher": { "version": "3.0.0", @@ -3310,11 +4542,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" }, "node_modules/merge-stream": { "version": "2.0.0", @@ -3322,6 +4565,30 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -3334,6 +4601,29 @@ "node": ">=4" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -3362,6 +4652,16 @@ "dev": true, "optional": true }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -3370,10 +4670,11 @@ "optional": true }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.8", @@ -3442,6 +4743,106 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-sarif-builder": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.2.0.tgz", + "integrity": "sha512-kVIOdynrF2CRodHZeP/97Rh1syTUHBNiw17hUCIVhlhEsWlfJm19MuO56s4MdKbr22xWx6mzMnNAgXzVlIYM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-sarif-builder/node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/node-sarif-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/node-sarif-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3485,10 +4886,11 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3556,6 +4958,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -3626,17 +5079,65 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz", + "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA= sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "devOptional": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -3695,6 +5196,16 @@ "node": ">=0.10.0" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/prebuild-install": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", @@ -3760,13 +5271,24 @@ "once": "^1.3.1" } }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -3775,6 +5297,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -3803,6 +5346,39 @@ "rc": "cli.js" } }, + "node_modules/rc-config-loader": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.3.tgz", + "integrity": "sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "js-yaml": "^4.1.0", + "json5": "^2.2.2", + "require-from-string": "^2.0.2" + } + }, + "node_modules/rc-config-loader/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/rc-config-loader/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -3815,6 +5391,52 @@ "node": ">=0.8" } }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -3856,6 +5478,16 @@ "node": ">= 0.10" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -3874,6 +5506,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -3892,6 +5535,30 @@ "node": ">=8.0" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -3904,6 +5571,28 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/semaphore": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", @@ -3945,23 +5634,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3984,15 +5656,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4001,6 +5675,75 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -4048,6 +5791,73 @@ "simple-concat": "^1.0.0" } }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4057,6 +5867,42 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/sprintf-js": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", @@ -4095,6 +5941,123 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", @@ -4149,6 +6112,16 @@ "dev": true, "license": "MIT" }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -4173,6 +6146,86 @@ "node": ">=4" } }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tar-fs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", @@ -4204,6 +6257,23 @@ "node": ">=6" } }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ternary-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-3.0.0.tgz", @@ -4226,6 +6296,29 @@ "readable-stream": "2 || 3" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -4366,6 +6459,7 @@ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } @@ -4401,6 +6495,7 @@ "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", "dev": true, + "license": "MIT", "dependencies": { "qs": "^6.9.1", "tunnel": "0.0.6", @@ -4408,16 +6503,18 @@ } }, "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" }, "node_modules/underscore": { "version": "1.13.7", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/undici-types": { "version": "6.20.0", @@ -4426,6 +6523,19 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", @@ -4462,6 +6572,30 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/vinyl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", @@ -4622,6 +6756,140 @@ "integrity": "sha512-r64Ea3glXY2RVzMeNxB+4J+0YHAVzUdV4cM5nHi4BBC2LvnO1pWFAIYKYuGcPElbg1/7eEiaPtZ/jzCjIUuGBg==", "dev": true }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/build/package.json b/build/package.json index 54ada234d79..2ea36955282 100644 --- a/build/package.json +++ b/build/package.json @@ -43,7 +43,7 @@ "@types/xml2js": "0.0.33", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/ripgrep": "^1.15.13", - "@vscode/vsce": "2.20.1", + "@vscode/vsce": "3.6.1", "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", @@ -71,5 +71,10 @@ "optionalDependencies": { "tree-sitter-typescript": "^0.23.2", "vscode-gulp-watch": "^5.0.3" + }, + "overrides": { + "path-scurry": { + "lru-cache": "11.2.1" + } } } diff --git a/build/setup-npm-registry.js b/build/setup-npm-registry.js index 5d637f97632..07bcf2296fa 100644 --- a/build/setup-npm-registry.js +++ b/build/setup-npm-registry.js @@ -34,7 +34,7 @@ async function* getPackageLockFiles(dir) { */ async function setup(url, file) { let contents = await fs.readFile(file, 'utf8'); - contents = contents.replace(/https:\/\/registry\.[^.]+\.com\//g, url); + contents = contents.replace(/https:\/\/registry\.[^.]+\.org\//g, url); await fs.writeFile(file, contents); } diff --git a/eslint.config.js b/eslint.config.js index 7d9f93f5f32..127bb34621f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -86,6 +86,8 @@ export default tseslint.config( 'local/code-no-unexternalized-strings': 'warn', 'local/code-must-use-super-dispose': 'warn', 'local/code-declare-service-brand': 'warn', + 'local/code-no-reader-after-await': 'warn', + 'local/code-no-observable-get-in-reactive-context': 'warn', 'local/code-no-deep-import-of-internal': ['error', { '.*Internal': true, 'searchExtTypesInternal': false }], 'local/code-layering': [ 'warn', @@ -307,7 +309,8 @@ export default tseslint.config( 'terminate', 'trigger', 'unregister', - 'write' + 'write', + 'commit' ] } ] @@ -824,6 +827,7 @@ export default tseslint.config( 'string_decoder', 'tas-client-umd', 'tls', + 'undici', 'undici-types', 'url', 'util', @@ -1421,6 +1425,7 @@ export default tseslint.config( { files: [ 'extensions/markdown-language-features/**/*.ts', + 'extensions/mermaid-chat-features/**/*.ts', 'extensions/media-preview/**/*.ts', 'extensions/simple-browser/**/*.ts', 'extensions/typescript-language-features/**/*.ts', @@ -1441,6 +1446,10 @@ export default tseslint.config( 'extensions/simple-browser/tsconfig.json', 'extensions/simple-browser/preview-src/tsconfig.json', + // Mermaid chat features + 'extensions/mermaid-chat-features/tsconfig.json', + 'extensions/mermaid-chat-features/chat-webview-src/tsconfig.json', + // TypeScript 'extensions/typescript-language-features/tsconfig.json', 'extensions/typescript-language-features/web/tsconfig.json', diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index a199e010be6..0fe0055e5a6 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -539,6 +539,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi if (textEditor === undefined) { commands.executeCommand('setContext', 'git.activeResourceHasUnstagedChanges', false); commands.executeCommand('setContext', 'git.activeResourceHasStagedChanges', false); + commands.executeCommand('setContext', 'git.activeResourceHasMergeConflicts', false); return; } @@ -546,6 +547,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi if (!repository) { commands.executeCommand('setContext', 'git.activeResourceHasUnstagedChanges', false); commands.executeCommand('setContext', 'git.activeResourceHasStagedChanges', false); + commands.executeCommand('setContext', 'git.activeResourceHasMergeConflicts', false); return; } @@ -553,9 +555,13 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi .find(resource => pathEquals(resource.resourceUri.fsPath, textEditor.document.uri.fsPath)); const workingTreeResource = repository.workingTreeGroup.resourceStates .find(resource => pathEquals(resource.resourceUri.fsPath, textEditor.document.uri.fsPath)); + const mergeChangesResource = repository.mergeGroup.resourceStates + .find(resource => pathEquals(resource.resourceUri.fsPath, textEditor.document.uri.fsPath)); + const hasMergeConflicts = mergeChangesResource ? /^(<{7,}|={7,}|>{7,})/m.test(textEditor.document.getText()) : false; commands.executeCommand('setContext', 'git.activeResourceHasStagedChanges', indexResource !== undefined); commands.executeCommand('setContext', 'git.activeResourceHasUnstagedChanges', workingTreeResource !== undefined); + commands.executeCommand('setContext', 'git.activeResourceHasMergeConflicts', hasMergeConflicts); } @sequentialize diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index a4c5036255f..730c76909b7 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef, l10n, workspace, Uri, DiagnosticSeverity, env } from 'vscode'; -import { dirname, sep, relative } from 'path'; +import { dirname, normalize, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; import byline from 'byline'; @@ -299,10 +299,16 @@ function normalizePath(path: string): string { // Windows & Mac are currently being handled // as case insensitive file systems in VS Code. if (isWindows || isMacintosh) { - return path.toLowerCase(); + path = path.toLowerCase(); } - return path; + // Remove trailing separator + if (path.charAt(path.length - 1) === sep) { + path = path.substring(0, path.length - 1); + } + + // Normalize the path + return normalize(path); } export function isDescendant(parent: string, descendant: string): boolean { @@ -310,11 +316,16 @@ export function isDescendant(parent: string, descendant: string): boolean { return true; } + // Normalize the paths + parent = normalizePath(parent); + descendant = normalizePath(descendant); + + // Ensure parent ends with separator if (parent.charAt(parent.length - 1) !== sep) { parent += sep; } - return normalizePath(descendant).startsWith(normalizePath(parent)); + return descendant.startsWith(parent); } export function pathEquals(a: string, b: string): boolean { diff --git a/extensions/mermaid-chat-features/.gitignore b/extensions/mermaid-chat-features/.gitignore new file mode 100644 index 00000000000..2877bd189bb --- /dev/null +++ b/extensions/mermaid-chat-features/.gitignore @@ -0,0 +1 @@ +chat-webview-out diff --git a/extensions/mermaid-chat-features/.npmrc b/extensions/mermaid-chat-features/.npmrc new file mode 100644 index 00000000000..a9c57709666 --- /dev/null +++ b/extensions/mermaid-chat-features/.npmrc @@ -0,0 +1,2 @@ +legacy-peer-deps="true" +timeout=180000 diff --git a/extensions/mermaid-chat-features/.vscodeignore b/extensions/mermaid-chat-features/.vscodeignore new file mode 100644 index 00000000000..4722e586990 --- /dev/null +++ b/extensions/mermaid-chat-features/.vscodeignore @@ -0,0 +1,8 @@ +src/** +extension.webpack.config.js +esbuild.* +cgmanifest.json +package-lock.json +webpack.config.js +tsconfig*.json +.gitignore diff --git a/extensions/mermaid-chat-features/README.md b/extensions/mermaid-chat-features/README.md new file mode 100644 index 00000000000..4df5d17d156 --- /dev/null +++ b/extensions/mermaid-chat-features/README.md @@ -0,0 +1,5 @@ +# Mermaid Chat Features + +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +Adds basic [Mermaid.js](https://mermaid.js.org) diagram rendering to build-in chat. diff --git a/extensions/mermaid-chat-features/cgmanifest.json b/extensions/mermaid-chat-features/cgmanifest.json new file mode 100644 index 00000000000..0c39c97297b --- /dev/null +++ b/extensions/mermaid-chat-features/cgmanifest.json @@ -0,0 +1,4 @@ +{ + "registrations": [], + "version": 1 +} diff --git a/extensions/mermaid-chat-features/chat-webview-src/index.ts b/extensions/mermaid-chat-features/chat-webview-src/index.ts new file mode 100644 index 00000000000..9b3c9df71b6 --- /dev/null +++ b/extensions/mermaid-chat-features/chat-webview-src/index.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import mermaid, { MermaidConfig } from 'mermaid'; + +function getMermaidTheme() { + return document.body.classList.contains('vscode-dark') || document.body.classList.contains('vscode-high-contrast') + ? 'dark' + : 'default'; +} + +type State = { + readonly diagramText: string; + readonly theme: 'dark' | 'default'; +}; + +let state: State | undefined = undefined; + +function init() { + const diagram = document.querySelector('.mermaid'); + if (!diagram) { + return; + } + + const theme = getMermaidTheme(); + state = { + diagramText: diagram.textContent ?? '', + theme + }; + + const config: MermaidConfig = { + startOnLoad: true, + theme, + }; + mermaid.initialize(config); +} + +function tryUpdate() { + const newTheme = getMermaidTheme(); + if (state?.theme === newTheme) { + return; + } + + const diagramNode = document.querySelector('.mermaid'); + if (!diagramNode || !(diagramNode instanceof HTMLElement)) { + return; + } + + state = { + diagramText: state?.diagramText ?? '', + theme: newTheme + }; + + // Re-render + diagramNode.textContent = state?.diagramText ?? ''; + delete diagramNode.dataset.processed; + + mermaid.initialize({ + theme: newTheme, + }); + mermaid.run({ + nodes: [diagramNode] + }); +} + +// Update when theme changes +new MutationObserver(() => { + tryUpdate(); +}).observe(document.body, { attributes: true, attributeFilter: ['class'] }); + +init(); + diff --git a/extensions/mermaid-chat-features/chat-webview-src/tsconfig.json b/extensions/mermaid-chat-features/chat-webview-src/tsconfig.json new file mode 100644 index 00000000000..72282fb0c7d --- /dev/null +++ b/extensions/mermaid-chat-features/chat-webview-src/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist/", + "jsx": "react", + "lib": [ + "ES2024", + "DOM", + "DOM.Iterable" + ] + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/tokens/markdownExtensionsToken.ts b/extensions/mermaid-chat-features/esbuild-chat-webview.mjs similarity index 51% rename from src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/tokens/markdownExtensionsToken.ts rename to extensions/mermaid-chat-features/esbuild-chat-webview.mjs index 82046eb2b4d..b23de5746fa 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/tokens/markdownExtensionsToken.ts +++ b/extensions/mermaid-chat-features/esbuild-chat-webview.mjs @@ -2,10 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// @ts-check +import path from 'path'; +import { run } from '../esbuild-webview-common.mjs'; -import { MarkdownToken } from '../../markdownCodec/tokens/markdownToken.js'; +const srcDir = path.join(import.meta.dirname, 'chat-webview-src'); +const outDir = path.join(import.meta.dirname, 'chat-webview-out'); -/** - * Base class for all tokens produced by the `MarkdownExtensionsDecoder`. - */ -export abstract class MarkdownExtensionsToken extends MarkdownToken { } +run({ + entryPoints: [ + path.join(srcDir, 'index.ts'), + ], + srcDir, + outdir: outDir, +}, process.argv); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptToken.ts b/extensions/mermaid-chat-features/extension-browser.webpack.config.js similarity index 62% rename from src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptToken.ts rename to extensions/mermaid-chat-features/extension-browser.webpack.config.js index 021b8d5a425..b758f2d8155 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptToken.ts +++ b/extensions/mermaid-chat-features/extension-browser.webpack.config.js @@ -2,10 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// @ts-check +import { browser as withBrowserDefaults } from '../shared.webpack.config.mjs'; -import { BaseToken } from '../base/baseToken.js'; - -/** - * Common base token that all chatbot `prompt` tokens should inherit from. - */ -export abstract class PromptToken extends BaseToken { } +export default withBrowserDefaults({ + context: import.meta.dirname, + entry: { + extension: './src/extension.ts' + } +}); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownToken.ts b/extensions/mermaid-chat-features/extension.webpack.config.js similarity index 60% rename from src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownToken.ts rename to extensions/mermaid-chat-features/extension.webpack.config.js index fc1935d081b..4928186ae55 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownToken.ts +++ b/extensions/mermaid-chat-features/extension.webpack.config.js @@ -2,11 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// @ts-check +import withDefaults from '../shared.webpack.config.mjs'; -import { BaseToken } from '../../baseToken.js'; - -/** - * Common base token that all `markdown` tokens should - * inherit from. - */ -export abstract class MarkdownToken extends BaseToken { } +export default withDefaults({ + context: import.meta.dirname, + resolve: { + mainFields: ['module', 'main'] + }, + entry: { + extension: './src/extension.ts', + } +}); diff --git a/extensions/mermaid-chat-features/package-lock.json b/extensions/mermaid-chat-features/package-lock.json new file mode 100644 index 00000000000..38a52f7fe20 --- /dev/null +++ b/extensions/mermaid-chat-features/package-lock.json @@ -0,0 +1,1433 @@ +{ + "name": "marmaid-chat-features", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "marmaid-chat-features", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "dompurify": "^3.2.6", + "mermaid": "^11.11.0" + }, + "devDependencies": { + "@types/jsdom": "^21.1.7", + "@types/node": "^24" + }, + "engines": { + "vscode": "^1.104.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-9.2.0.tgz", + "integrity": "sha512-Oq1d9BGZakE/FyoEtcNeSwM7MpDO2vUBi11RWBZXf75zPsbUVWmUs03EqkRFrcgbXyKTas0BdZWC1wcuSoqSAw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.1.tgz", + "integrity": "sha512-A78CUEnFGX8I/WlILxJCuIJXloL0j/OJ9PSchPAfCargEIKmUBWvvEMmKWB5oONwiUqlNt+5eRufdkLxeHIWYw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@antfu/utils": "^9.2.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.1", + "globals": "^15.15.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.1.1", + "mlly": "^1.7.4" + } + }, + "node_modules/@iconify/utils/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz", + "integrity": "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.0.tgz", + "integrity": "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "license": "MIT" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", + "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "license": "MIT" + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/katex": { + "version": "0.16.22", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "license": "MIT" + }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mermaid": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.11.0.tgz", + "integrity": "sha512-9lb/VNkZqWTRjVgCV+l1N+t4kyi94y+l5xrmBmbbxZYkfRl5hEDaTPMOcaWKCl1McG8nBEaMlWwkcAEEgjhBgg==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.0.4", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^0.6.2", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.11", + "dayjs": "^1.11.13", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^15.0.7", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/package-manager-detector": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", + "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "license": "MIT" + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + } + } +} diff --git a/extensions/mermaid-chat-features/package.json b/extensions/mermaid-chat-features/package.json new file mode 100644 index 00000000000..436a4cc1fb3 --- /dev/null +++ b/extensions/mermaid-chat-features/package.json @@ -0,0 +1,74 @@ +{ + "name": "marmaid-chat-features", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "vscode", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + }, + "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", + "engines": { + "vscode": "^1.104.0" + }, + "enabledApiProposals": [ + "chatOutputRenderer" + ], + "capabilities": { + "virtualWorkspaces": true, + "untrustedWorkspaces": { + "supported": true + } + }, + "main": "./out/extension", + "browser": "./dist/browser/extension", + "activationEvents": [], + "contributes": { + "chatOutputRenderers": [ + { + "viewType": "vscode.chatMermaidDiagram", + "mimeTypes": [ + "text/vnd.mermaid" + ] + } + ], + "languageModelTools": [ + { + "name": "renderMermaidDiagram", + "displayName": "Mermaid Renderer", + "toolReferenceName": "renderMermaidDiagram", + "canBeReferencedInPrompt": true, + "modelDescription": "Renders a Mermaid diagram from Mermaid.js markup.", + "userDescription": "Render a Mermaid.js diagrams from markup.", + "inputSchema": { + "type": "object", + "properties": { + "markup": { + "type": "string", + "description": "The mermaid diagram markup to render as a Mermaid diagram. This should only be the markup of the diagram. Do not include a wrapping code block." + } + } + } + } + ] + }, + "scripts": { + "compile": "gulp compile-extension:mermaid-chat-features && npm run build-chat-webview", + "watch": "npm run build-chat-webview && gulp watch-extension:mermaid-chat-features", + "vscode:prepublish": "npm run build-ext && npm run build-chat-webview", + "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:mermaid-chat-features", + "build-chat-webview": "node ./esbuild-chat-webview.mjs", + "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", + "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" + }, + "devDependencies": { + "@types/jsdom": "^21.1.7", + "@types/node": "^24" + }, + "dependencies": { + "dompurify": "^3.2.6", + "mermaid": "^11.11.0" + } +} diff --git a/extensions/mermaid-chat-features/package.nls.json b/extensions/mermaid-chat-features/package.nls.json new file mode 100644 index 00000000000..882e5d59f01 --- /dev/null +++ b/extensions/mermaid-chat-features/package.nls.json @@ -0,0 +1,4 @@ +{ + "displayName": "Mermaid Chat Features", + "description": "Adds Mermaid diagram support to built-in chats." +} diff --git a/extensions/mermaid-chat-features/src/extension.ts b/extensions/mermaid-chat-features/src/extension.ts new file mode 100644 index 00000000000..d66518a8db3 --- /dev/null +++ b/extensions/mermaid-chat-features/src/extension.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; + +/** + * View type that uniquely identifies the Mermaid chat output renderer. + */ +const viewType = 'vscode.chatMermaidDiagram'; + +/** + * Mime type used to identify Mermaid diagram data in chat output. + */ +const mime = 'text/vnd.mermaid'; + +export function activate(context: vscode.ExtensionContext) { + + // Register tools + context.subscriptions.push( + vscode.lm.registerTool<{ markup: string }>('renderMermaidDiagram', { + invoke: async (options, _token) => { + const sourceCode = options.input.markup; + return writeMermaidToolOutput(sourceCode); + }, + }) + ); + + // Register the chat output renderer for Mermaid diagrams. + // This will be invoked with the data generated by the tools. + // It can also be invoked when rendering old Mermaid diagrams in the chat history. + context.subscriptions.push( + vscode.chat.registerChatOutputRenderer(viewType, { + async renderChatOutput({ value }, webview, _ctx, _token) { + const mermaidSource = new TextDecoder().decode(value); + + // Set the options for the webview + const mediaRoot = vscode.Uri.joinPath(context.extensionUri, 'chat-webview-out'); + webview.options = { + enableScripts: true, + localResourceRoots: [mediaRoot], + }; + + // Set the HTML content for the webview + const nonce = getNonce(); + const mermaidScript = vscode.Uri.joinPath(mediaRoot, 'index.js'); + + webview.html = ` + + + + + + + Mermaid Diagram + + + + +
+							${escapeHtmlText(mermaidSource)}
+						
+ + + + `; + }, + })); +} + + +function writeMermaidToolOutput(sourceCode: string): vscode.LanguageModelToolResult { + // Expose the source code as a tool result for the LM + const result = new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart(sourceCode) + ]); + + // And store custom data in the tool result details to indicate that a custom renderer should be used for it. + // In this case we just store the source code as binary data. + + // Add cast to use proposed API + (result as vscode.ExtendedLanguageModelToolResult2).toolResultDetails2 = { + mime, + value: new TextEncoder().encode(sourceCode), + }; + + return result; +} + +function escapeHtmlText(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function getNonce() { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 64; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} diff --git a/extensions/mermaid-chat-features/tsconfig.json b/extensions/mermaid-chat-features/tsconfig.json new file mode 100644 index 00000000000..11e3e9a5d5f --- /dev/null +++ b/extensions/mermaid-chat-features/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + }, + "include": [ + "src/**/*", + "../../src/vscode-dts/vscode.d.ts", + "../../src/vscode-dts/vscode.proposed.chatOutputRenderer.d.ts", + "../../src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts", + "../../src/vscode-dts/vscode.proposed.languageModelThinkingPart.d.ts", + "../../src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts" + ] +} diff --git a/extensions/microsoft-authentication/package-lock.json b/extensions/microsoft-authentication/package-lock.json index c3b427c607f..80f81423ee7 100644 --- a/extensions/microsoft-authentication/package-lock.json +++ b/extensions/microsoft-authentication/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@azure/ms-rest-azure-env": "^2.0.0", - "@azure/msal-node": "^3.7.3", + "@azure/msal-node": "^3.7.4", "@azure/msal-node-extensions": "^1.5.22", "@vscode/extension-telemetry": "^0.9.8", "keytar": "file:./packageMocks/keytar", @@ -42,9 +42,9 @@ } }, "node_modules/@azure/msal-node": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.7.3.tgz", - "integrity": "sha512-MoJxkKM/YpChfq4g2o36tElyzNUMG8mfD6u8NbuaPAsqfGpaw249khAcJYNoIOigUzRw45OjXCOrexE6ImdUxg==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.7.4.tgz", + "integrity": "sha512-fjqvhrThwzzPvqhFOdkkGRJCHPQZTNijpceVy8QjcfQuH482tOVEjHyamZaioOhVtx+FK1u+eMpJA2Zz4U9LVg==", "license": "MIT", "dependencies": { "@azure/msal-common": "15.12.0", @@ -71,9 +71,9 @@ } }, "node_modules/@azure/msal-node-runtime": { - "version": "0.19.4", - "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.19.4.tgz", - "integrity": "sha512-v90QdV/VKG6gvHx2bQp82yRCJ8IGG7OOk6gDQgbKvoHtOs7mSEz2CIqWydUpwCryJkUwgNfgMPAMoGhe/a4fOw==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.19.5.tgz", + "integrity": "sha512-0oBQgCcgOb+VwQ5k8OXShbuXCBU8FKKhpwnqWSBzzYWSFoYAtyad2zggl26ME4IKzN9telaOJPEEcsQOf/+3Ug==", "hasInstallScript": true, "license": "MIT" }, diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 01371bb83bc..65290e3767e 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -147,7 +147,7 @@ }, "dependencies": { "@azure/ms-rest-azure-env": "^2.0.0", - "@azure/msal-node": "^3.7.3", + "@azure/msal-node": "^3.7.4", "@azure/msal-node-extensions": "^1.5.22", "@vscode/extension-telemetry": "^0.9.8", "keytar": "file:./packageMocks/keytar", diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index 6c11983b6c3..5008ce7f82d 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -51,8 +51,8 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica const loggerOptions = new MsalLoggerOptions(_logger, telemetryReporter); let broker: BrokerOptions | undefined; - if (process.platform !== 'win32') { - this._logger.info(`[${this._clientId}] Native Broker is only available on Windows`); + if (process.platform !== 'win32' && process.platform !== 'darwin') { + this._logger.info(`[${this._clientId}] Native Broker is only available on Windows and macOS`); } else if (workspace.getConfiguration('microsoft-authentication').get<'msal' | 'msal-no-broker'>('implementation') === 'msal-no-broker') { this._logger.info(`[${this._clientId}] Native Broker disabled via settings`); } else { diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 789de38deb3..5e081c4bbd2 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -57,10 +57,10 @@ ] }, "scripts": { - "compile": "gulp compile-extension:markdown-language-features && npm run build-preview", - "watch": "npm run build-preview && gulp watch-extension:markdown-language-features", + "compile": "gulp compile-extension:simple-browser && npm run build-preview", + "watch": "npm run build-preview && gulp watch-extension:simple-browser", "vscode:prepublish": "npm run build-ext && npm run build-preview", - "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:markdown-language-features ./tsconfig.json", + "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:simple-browser ./tsconfig.json", "build-preview": "node ./esbuild-preview.mjs", "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index cc5247870d8..4f12b258652 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -306,7 +306,7 @@ export async function activate(context: vscode.ExtensionContext) { const cwd = result.cwd ?? terminal.shellIntegration?.cwd; if (cwd && (result.filesRequested || result.foldersRequested)) { - const globPattern = createFileRegex(result.fileExtensions); + const globPattern = createFileGlobPattern(result.fileExtensions); return new vscode.TerminalCompletionList(result.items, { filesRequested: result.filesRequested, foldersRequested: result.foldersRequested, @@ -564,17 +564,13 @@ export function sanitizeProcessEnvironment(env: Record, ...prese }); } - -// Escapes regex special characters in a string -function escapeRegExp(str: string): string { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -function createFileRegex(fileExtensions?: string[]): vscode.GlobPattern | undefined { +function createFileGlobPattern(fileExtensions?: string[]): vscode.GlobPattern | undefined { if (!fileExtensions || fileExtensions.length === 0) { return undefined; } - const exts = fileExtensions.map(ext => ext.startsWith('.') ? ext : '.' + ext); - // Create a regex that matches any string ending with one of the extensions - return `.*(${exts.map(ext => escapeRegExp(ext)).join('|')})$`; + const exts = fileExtensions.map(ext => ext.startsWith('.') ? ext.slice(1) : ext); + if (exts.length === 1) { + return `**/*.${exts[0]}`; + } + return `**/*.{${exts.join(',')}}`; } diff --git a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index 35ca543a8d4..c0e3221be2e 100644 --- a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -367,7 +367,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider=20.6.0" - }, "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250910.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250910.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20250910.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250910.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20250910.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250910.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20250910.1" + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250922.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250922.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20250922.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250922.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20250922.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250922.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20250922.1" } }, "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20250910.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20250910.1.tgz", - "integrity": "sha512-PcSdX+LDNSAsBCPcbLx01sE8kyNv48WAGlxtRptrXvgnY61O64FzqCr755pteGIGrEEEQ9qsmm5b68EdyMbqJQ==", + "version": "7.0.0-dev.20250922.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20250922.1.tgz", + "integrity": "sha512-R4sKE/cQYMGrmaz8OBnnseABdB6W0ZCQ9muHQnoxuyukf7m0wLLqvGNorQZSWVl9xQ7qNniXcZJlCKp4JmRr6w==", "cpu": [ "arm64" ], @@ -2502,15 +2500,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": ">=20.6.0" - } + ] }, "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20250910.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20250910.1.tgz", - "integrity": "sha512-iz4T7SmwoLczNxfIlEfb3WQSD/cpcybT27s0S/HOAJbrZRRDRT/RvNWLT9PI0aEVM7EGxHrPOExjWGErItGP/w==", + "version": "7.0.0-dev.20250922.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20250922.1.tgz", + "integrity": "sha512-Hhh+9e/75ZQxdatdA8TFKOKjPwqjXWNI2a8vswvMU6zOmiVYZoYaUGEGFhk0W1WPGnt/kZSM1Mb4a9nVaEwhZQ==", "cpu": [ "x64" ], @@ -2519,15 +2514,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": ">=20.6.0" - } + ] }, "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20250910.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20250910.1.tgz", - "integrity": "sha512-LfxNhRNuxBjfGWsOn/aQ3j76E+ZyyvMcQOVYYyr/+5BG/2bCtKtKtLI+xZzxeaPL6yz3a+TTdO1RtT79z7IKGw==", + "version": "7.0.0-dev.20250922.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20250922.1.tgz", + "integrity": "sha512-4JbSk4B0SUo2c+S9YKGLAaMQXA16Pd9Cd7GLhMiABRtOLEHFDAaFmFbVoGYX5Dyicix711oziX/WAhD6nuC73A==", "cpu": [ "arm" ], @@ -2536,15 +2528,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=20.6.0" - } + ] }, "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20250910.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20250910.1.tgz", - "integrity": "sha512-ezy44eOONVCTKBbMNuswVXQJDH96vZz1UttTzCWoLjnxa5YLPoSxtdXLzqLDXFm9mznFkO55dq5brFc0yEcytQ==", + "version": "7.0.0-dev.20250922.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20250922.1.tgz", + "integrity": "sha512-Rw74z0tf0NEujnl0W58fQ5pbh8P1fr2ujQTMkQYz9WpH+hYuysQgcOiDEoht62CxtLHflexu3dUWJI8/LUPyHA==", "cpu": [ "arm64" ], @@ -2553,15 +2542,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=20.6.0" - } + ] }, "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20250910.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250910.1.tgz", - "integrity": "sha512-pBmxFR9+C74QGzhDS8PRRBswQcnVFp2XD2J6oFfDcMmtsq8gFIlggAx3+k3fLysoH5gJDISsgjZr43IIcncpzQ==", + "version": "7.0.0-dev.20250922.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250922.1.tgz", + "integrity": "sha512-lY6JAZgzeJxLwciC8Wc9l7OlDYwVbuo3JK+qHfLlyxu8h1Q3cGF5VMpgSlAj9CKLDQ0h4kDAecLLiEEOod78aw==", "cpu": [ "x64" ], @@ -2570,15 +2556,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=20.6.0" - } + ] }, "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20250910.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20250910.1.tgz", - "integrity": "sha512-qCJYqZ/utGR9OPGx0871Ov1djUZ9MAFtGKrtbgUwrwjfXLLGZHUDOmI9brfYQ2ruCxTKENk6IfWIAtY+zV6C2A==", + "version": "7.0.0-dev.20250922.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20250922.1.tgz", + "integrity": "sha512-pGzWqNx7x6Sqs20Ea5Xn/JC9eZ9Do6bu6K+dmfpewYYhwUv8jiOhh+fr9TJFZ8MlaZDTp9FclQIGpW+2MZ8UOw==", "cpu": [ "arm64" ], @@ -2587,15 +2570,12 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=20.6.0" - } + ] }, "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20250910.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250910.1.tgz", - "integrity": "sha512-RPjV+ZypHeSz1Ky6wys52tXTYpYQ4fAqovC5ZZC+j+Lf8gEEcWXcmuJMEBN6vhTCvx1c6QdRtBBIYRrUHh/JEA==", + "version": "7.0.0-dev.20250922.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250922.1.tgz", + "integrity": "sha512-RoNm3P69p5F/ZJ7O7nIoUJB0OVG5nGIEVzP/VF2lm8jBbDN8aCRNT+DkZrQvdXJEBg2fM0WrTzcik9KVXAyEag==", "cpu": [ "x64" ], @@ -2604,10 +2584,7 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=20.6.0" - } + ] }, "node_modules/@vscode/deviceid": { "version": "0.1.1", @@ -3002,9 +2979,9 @@ } }, "node_modules/@vscode/proxy-agent": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.33.0.tgz", - "integrity": "sha512-pfGEfRySMAB0ZRk7NIiIpW5BWGMZvQCN6Jf30+uupl589P1cVQ+HD8WxFTxeJlz9wNyelJqAXzFt12IuB+D3Ew==", + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.34.0.tgz", + "integrity": "sha512-LrX5mb+0vgvGQ/1jLvpsd4tUzlCVYNjvu+vvPx+yV2AvyXXnRQj/Qom1Fiavw9Mfmxw3+AHfzZ73tXwTMCfEdQ==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", @@ -12916,9 +12893,9 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta33", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta33.tgz", - "integrity": "sha512-+BN2bT/KqO+fmCHnpFS99VMVJr7VUBCUa2VIBEw0oEvszkR7ri0kwD1lF91OeQToUJ2dXKA8j6scPjbO4eRWOQ==", + "version": "1.1.0-beta35", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta35.tgz", + "integrity": "sha512-dGKw3PtLj/+uiFWUODNjr3QMyNjxRB2JY372AN4uzonfb6ri23d4PMr4s6UoibiqsXOQ3elXRCdq1qDLd86J8Q==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -14335,9 +14312,9 @@ } }, "node_modules/prebuild-install/node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -16602,9 +16579,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.10.tgz", - "integrity": "sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, "license": "MIT", "dependencies": { @@ -17267,9 +17244,9 @@ "dev": true }, "node_modules/typescript": { - "version": "6.0.0-dev.20250910", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.0-dev.20250910.tgz", - "integrity": "sha512-c8xZmVV/0FVlBoiRveyYSSs2jL0AHRZ3m2fUPgpY9ZFh7edYiWlBsZ7zPZIHzdNieuJ5ryYENw4pbRasp2Fo/w==", + "version": "6.0.0-dev.20250922", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.0-dev.20250922.tgz", + "integrity": "sha512-4jTznRR2W8ak4kgHlxhNEauwCS/O2O2AfS3yC+Y4VxkRDFIruwdcW4+UQflBJrLCFa42lhdAAMGl1td/99KTKg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 6ad06e6a153..eece8d0d0bf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.105.0", - "distro": "8103a60ed6fb457dbf0ba180a67be3341dce4a23", + "distro": "35091a9a52c6140fcbee98e0829b9713aed0f6a0", "author": { "name": "Microsoft Corporation" }, @@ -38,6 +38,7 @@ "7z": "7z", "update-grammars": "node build/npm/update-all-grammars.mjs", "update-localization-extension": "node build/npm/update-localization-extension.js", + "mixin-telemetry-docs": "node build/npm/mixin-telemetry-docs.mjs", "smoketest": "node build/lib/preLaunch.js && cd test/smoke && npm run compile && node test/index.js", "smoketest-no-compile": "cd test/smoke && node test/index.js", "download-builtin-extensions": "node build/lib/builtInExtensions.js", @@ -77,7 +78,7 @@ "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.3.2", - "@vscode/proxy-agent": "^0.33.0", + "@vscode/proxy-agent": "^0.34.0", "@vscode/ripgrep": "^1.15.13", "@vscode/spdlog": "^0.15.2", "@vscode/sqlite3": "5.1.8-vscode", @@ -106,9 +107,10 @@ "native-is-elevated": "0.7.0", "native-keymap": "^3.3.5", "native-watchdog": "^1.4.1", - "node-pty": "^1.1.0-beta33", + "node-pty": "1.1.0-beta35", "open": "^10.1.2", "tas-client-umd": "0.2.0", + "undici": "^7.9.0", "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", @@ -211,7 +213,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^6.0.0-dev.20250910", + "typescript": "^6.0.0-dev.20250922", "typescript-eslint": "^8.39.0", "util": "^0.12.4", "webpack": "^5.94.0", diff --git a/remote/package-lock.json b/remote/package-lock.json index a402325e80e..0512291031e 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -13,7 +13,7 @@ "@parcel/watcher": "parcel-bundler/watcher#1ca032aa8339260a8a3bcf825c3a1a71e3e43542", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.33.0", + "@vscode/proxy-agent": "^0.34.0", "@vscode/ripgrep": "^1.15.13", "@vscode/spdlog": "^0.15.2", "@vscode/tree-sitter-wasm": "^0.1.4", @@ -38,7 +38,7 @@ "kerberos": "2.1.1", "minimist": "^1.2.8", "native-watchdog": "^1.4.1", - "node-pty": "^1.1.0-beta33", + "node-pty": "1.1.0-beta35", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", @@ -133,9 +133,9 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" }, "node_modules/@vscode/proxy-agent": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.33.0.tgz", - "integrity": "sha512-pfGEfRySMAB0ZRk7NIiIpW5BWGMZvQCN6Jf30+uupl589P1cVQ+HD8WxFTxeJlz9wNyelJqAXzFt12IuB+D3Ew==", + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.34.0.tgz", + "integrity": "sha512-LrX5mb+0vgvGQ/1jLvpsd4tUzlCVYNjvu+vvPx+yV2AvyXXnRQj/Qom1Fiavw9Mfmxw3+AHfzZ73tXwTMCfEdQ==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", @@ -842,9 +842,9 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta33", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta33.tgz", - "integrity": "sha512-+BN2bT/KqO+fmCHnpFS99VMVJr7VUBCUa2VIBEw0oEvszkR7ri0kwD1lF91OeQToUJ2dXKA8j6scPjbO4eRWOQ==", + "version": "1.1.0-beta35", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta35.tgz", + "integrity": "sha512-dGKw3PtLj/+uiFWUODNjr3QMyNjxRB2JY372AN4uzonfb6ri23d4PMr4s6UoibiqsXOQ3elXRCdq1qDLd86J8Q==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1095,9 +1095,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", diff --git a/remote/package.json b/remote/package.json index e4a0457c914..9af773e9a1c 100644 --- a/remote/package.json +++ b/remote/package.json @@ -8,7 +8,7 @@ "@parcel/watcher": "parcel-bundler/watcher#1ca032aa8339260a8a3bcf825c3a1a71e3e43542", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.33.0", + "@vscode/proxy-agent": "^0.34.0", "@vscode/ripgrep": "^1.15.13", "@vscode/spdlog": "^0.15.2", "@vscode/tree-sitter-wasm": "^0.1.4", @@ -33,7 +33,7 @@ "kerberos": "2.1.1", "minimist": "^1.2.8", "native-watchdog": "^1.4.1", - "node-pty": "^1.1.0-beta33", + "node-pty": "1.1.0-beta35", "tas-client-umd": "0.2.0", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index f5948f40879..5fd5c8bf45e 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1602,7 +1602,7 @@ export async function triggerNotification(message: string, options?: { detail?: const notification = new Notification(message, { body: options?.detail, - requireInteraction: options?.sticky + requireInteraction: options?.sticky, }); const onClick = new event.Emitter(); diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 71af7a09743..87ec4b8ec64 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -16,7 +16,7 @@ import { FileAccess, Schemas } from '../common/network.js'; import { cloneAndChange } from '../common/objects.js'; import { dirname, resolvePath } from '../common/resources.js'; import { escape } from '../common/strings.js'; -import { URI } from '../common/uri.js'; +import { URI, UriComponents } from '../common/uri.js'; import * as DOM from './dom.js'; import * as domSanitize from './domSanitize.js'; import { convertTagToPlaintext } from './domSanitize.js'; @@ -151,7 +151,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende } const renderedContent = document.createElement('div'); - const sanitizerConfig = getDomSanitizerConfig(markdown.isTrusted ?? false, options.sanitizerConfig ?? {}); + const sanitizerConfig = getDomSanitizerConfig(markdown, options.sanitizerConfig ?? {}); domSanitize.safeSetInnerHtml(renderedContent, renderedMarkdown, sanitizerConfig); // Rewrite links and images before potentially inserting them into the real dom @@ -437,12 +437,17 @@ function resolveWithBaseUri(baseUri: URI, href: string): string { } } +type MdStrConfig = { + readonly isTrusted?: boolean | MarkdownStringTrustedOptions; + readonly baseUri?: UriComponents; +}; + function sanitizeRenderedMarkdown( renderedMarkdown: string, - isTrusted: boolean | MarkdownStringTrustedOptions, + originalMdStrConfig: MdStrConfig, options: MarkdownSanitizerConfig = {}, ): TrustedHTML { - const sanitizerConfig = getDomSanitizerConfig(isTrusted, options); + const sanitizerConfig = getDomSanitizerConfig(originalMdStrConfig, options); return domSanitize.sanitizeHtml(renderedMarkdown, sanitizerConfig); } @@ -508,7 +513,8 @@ export const allowedMarkdownHtmlAttributes = Object.freeze unescapeInfo.get(m) ?? m) .trim(); diff --git a/src/vs/base/common/cancellation.ts b/src/vs/base/common/cancellation.ts index c5b8133ae6c..3be4a90a103 100644 --- a/src/vs/base/common/cancellation.ts +++ b/src/vs/base/common/cancellation.ts @@ -146,3 +146,61 @@ export function cancelOnDispose(store: DisposableStore): CancellationToken { store.add({ dispose() { source.cancel(); } }); return source.token; } + +/** + * A pool that aggregates multiple cancellation tokens. The pool's own token + * (accessible via `pool.token`) is cancelled only after every token added + * to the pool has been cancelled. Adding tokens after the pool token has + * been cancelled has no effect. + */ +export class CancellationTokenPool { + + private readonly _source = new CancellationTokenSource(); + private readonly _listeners = new DisposableStore(); + + private _total: number = 0; + private _cancelled: number = 0; + private _isDone: boolean = false; + + get token(): CancellationToken { + return this._source.token; + } + + /** + * Add a token to the pool. If the token is already cancelled it is counted + * immediately. Tokens added after the pool token has been cancelled are ignored. + */ + add(token: CancellationToken): void { + if (this._isDone) { + return; + } + + this._total++; + + if (token.isCancellationRequested) { + this._cancelled++; + this._check(); + return; + } + + const d = token.onCancellationRequested(() => { + d.dispose(); + this._cancelled++; + this._check(); + }); + this._listeners.add(d); + } + + private _check(): void { + if (!this._isDone && this._total > 0 && this._total === this._cancelled) { + this._isDone = true; + this._listeners.dispose(); + this._source.cancel(); + } + } + + dispose(): void { + this._listeners.dispose(); + this._source.dispose(); + } +} diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index 99ddaa059c7..b71901416d9 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -200,11 +200,14 @@ export function parseHrefAndDimensions(href: string): { href: string; dimensions } export function markdownCommandLink(command: { title: string; id: string; arguments?: unknown[] }, escapeTokens = true): string { - const uri = URI.from({ - scheme: Schemas.command, - path: command.id, - query: command.arguments?.length ? encodeURIComponent(JSON.stringify(command.arguments)) : undefined, - }).toString(); - + const uri = createCommandUri(command.id, ...(command.arguments || [])).toString(); return `[${escapeTokens ? escapeMarkdownSyntaxTokens(command.title) : command.title}](${uri})`; } + +export function createCommandUri(commandId: string, ...commandArgs: unknown[]): URI { + return URI.from({ + scheme: Schemas.command, + path: commandId, + query: commandArgs.length ? encodeURIComponent(JSON.stringify(commandArgs)) : undefined, + }); +} diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 90264b8371e..8715843777e 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -367,19 +367,36 @@ export function combinedDisposable(...disposables: IDisposable[]): IDisposable { return parent; } +class FunctionDisposable implements IDisposable { + private _isDisposed: boolean; + private readonly _fn: () => void; + + constructor(fn: () => void) { + this._isDisposed = false; + this._fn = fn; + trackDisposable(this); + } + + dispose() { + if (this._isDisposed) { + return; + } + if (!this._fn) { + throw new Error(`Unbound disposable context: Need to use an arrow function to preserve the value of this`); + } + this._isDisposed = true; + markAsDisposed(this); + this._fn(); + } +} + /** * Turn a function that implements dispose into an {@link IDisposable}. * * @param fn Clean up function, guaranteed to be called only **once**. */ export function toDisposable(fn: () => void): IDisposable { - const self = trackDisposable({ - dispose: createSingleCallFunction(() => { - markAsDisposed(self); - fn(); - }) - }); - return self; + return new FunctionDisposable(fn); } /** diff --git a/src/vs/base/common/numbers.ts b/src/vs/base/common/numbers.ts index 89c9f183e6d..326a9b2f2c3 100644 --- a/src/vs/base/common/numbers.ts +++ b/src/vs/base/common/numbers.ts @@ -99,65 +99,6 @@ export function isPointWithinTriangle( return u >= 0 && v >= 0 && u + v < 1; } -/** - * Function to get a (pseudo)random integer from a provided `max`...[`min`] range. - * Both `min` and `max` values are inclusive. The `min` value is optional (defaults to `0`). - * - * @throws in the next cases: - * - if provided `min` or `max` is not a number - * - if provided `min` or `max` is not finite - * - if provided `min` is larger than `max` value - * - * ## Examples - * - * Specifying a `max` value only uses `0` as the `min` value by default: - * - * ```typescript - * // get a random integer between 0 and 10 - * const randomInt = randomInt(10); - * - * assert( - * randomInt >= 0, - * 'Should be greater than or equal to 0.', - * ); - * - * assert( - * randomInt <= 10, - * 'Should be less than or equal to 10.', - * ); - * ``` - * * Specifying both `max` and `min` values: - * - * ```typescript - * // get a random integer between 5 and 8 - * const randomInt = randomInt(8, 5); - * - * assert( - * randomInt >= 5, - * 'Should be greater than or equal to 5.', - * ); - * - * assert( - * randomInt <= 8, - * 'Should be less than or equal to 8.', - * ); - * ``` - */ -export function randomInt(max: number, min: number = 0): number { - assert(!isNaN(min), '"min" param is not a number.'); - assert(!isNaN(max), '"max" param is not a number.'); - - assert(isFinite(max), '"max" param is not finite.'); - assert(isFinite(min), '"min" param is not finite.'); - - assert(max > min, `"max"(${max}) param should be greater than "min"(${min}).`); - - const delta = max - min; - const randomFloat = delta * Math.random(); - - return Math.round(min + randomFloat); -} - export function randomChance(p: number): boolean { assert(p >= 0 && p <= 1, 'p must be between 0 and 1'); return Math.random() < p; diff --git a/src/vs/base/common/oauth.ts b/src/vs/base/common/oauth.ts index c4d5a755edd..ce4889265b2 100644 --- a/src/vs/base/common/oauth.ts +++ b/src/vs/base/common/oauth.ts @@ -764,13 +764,11 @@ export async function fetchDynamicRegistration(serverMetadata: IAuthorizationSer redirect_uris: [ 'https://insiders.vscode.dev/redirect', 'https://vscode.dev/redirect', - 'http://localhost/', 'http://127.0.0.1/', // Added these for any server that might do // only exact match on the redirect URI even // though the spec says it should not care // about the port. - `http://localhost:${DEFAULT_AUTH_FLOW_PORT}/`, `http://127.0.0.1:${DEFAULT_AUTH_FLOW_PORT}/` ], scope: scopes?.join(AUTH_SCOPE_SEPARATOR), diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index cf76f7ce0e1..8c0d6c319e9 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -278,3 +278,7 @@ export const isAndroid = !!(userAgent && userAgent.indexOf('Android') >= 0); export function isBigSurOrNewer(osVersion: string): boolean { return parseFloat(osVersion) >= 20; } + +export function isTahoe(osVersion: string): boolean { + return parseFloat(osVersion) === 25; +} diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 88f389acae2..d52be9f2cce 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -379,6 +379,7 @@ export interface IDefaultChatAgent { readonly completionsRefreshTokenCommand: string; readonly chatRefreshTokenCommand: string; readonly generateCommitMessageCommand: string; + readonly resolveMergeConflictsCommand: string; readonly completionsAdvancedSetting: string; readonly completionsEnablementSetting: string; diff --git a/src/vs/workbench/api/node/uriTransformer.ts b/src/vs/base/common/uriTransformer.ts similarity index 97% rename from src/vs/workbench/api/node/uriTransformer.ts rename to src/vs/base/common/uriTransformer.ts index 00bf30130df..40451e04db8 100644 --- a/src/vs/workbench/api/node/uriTransformer.ts +++ b/src/vs/base/common/uriTransformer.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { UriParts, IRawURITransformer, URITransformer, IURITransformer } from '../../../base/common/uriIpc.js'; +import { UriParts, IRawURITransformer, URITransformer, IURITransformer } from './uriIpc.js'; /** * ``` diff --git a/src/vs/base/common/yaml.ts b/src/vs/base/common/yaml.ts new file mode 100644 index 00000000000..490d068e0f0 --- /dev/null +++ b/src/vs/base/common/yaml.ts @@ -0,0 +1,832 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Parses a simplified YAML-like input from a single string. + * Supports objects, arrays, primitive types (string, number, boolean, null). + * Tracks positions for error reporting and node locations. + * + * Limitations: + * - No multi-line strings or block literals + * - No anchors or references + * - No complex types (dates, binary) + * - No special handling for escape sequences in strings + * - Indentation must be consistent (spaces only, no tabs) + * + * Notes: + * - New line separators can be either "\n" or "\r\n". The input string is split into lines internally. + * + * @param input A string containing the YAML-like input + * @param errors Array to collect parsing errors + * @param options Parsing options + * @returns The parsed representation (ObjectNode, ArrayNode, or primitive node) + */ +export function parse(input: string, errors: YamlParseError[] = [], options: ParseOptions = {}): YamlNode | undefined { + // Normalize both LF and CRLF by splitting on either; CR characters are not retained as part of line text. + // This keeps the existing line/character based lexer logic intact. + const lines = input.length === 0 ? [] : input.split(/\r\n|\n/); + const parser = new YamlParser(lines, errors, options); + return parser.parse(); +} + +export interface YamlParseError { + readonly message: string; + readonly start: Position; + readonly end: Position; + readonly code: string; +} + +export interface ParseOptions { + readonly allowDuplicateKeys?: boolean; +} + +export interface Position { + readonly line: number; + readonly character: number; +} + +export interface YamlStringNode { + readonly type: 'string'; + readonly value: string; + readonly start: Position; + readonly end: Position; +} + +export interface YamlNumberNode { + readonly type: 'number'; + readonly value: number; + readonly start: Position; + readonly end: Position; +} + +export interface YamlBooleanNode { + readonly type: 'boolean'; + readonly value: boolean; + readonly start: Position; + readonly end: Position; +} + +export interface YamlNullNode { + readonly type: 'null'; + readonly value: null; + readonly start: Position; + readonly end: Position; +} + +export interface YamlObjectNode { + readonly type: 'object'; + readonly properties: { key: YamlStringNode; value: YamlNode }[]; + readonly start: Position; + readonly end: Position; +} + +export interface YamlArrayNode { + readonly type: 'array'; + readonly items: YamlNode[]; + readonly start: Position; + readonly end: Position; +} + +export type YamlNode = YamlStringNode | YamlNumberNode | YamlBooleanNode | YamlNullNode | YamlObjectNode | YamlArrayNode; + +// Helper functions for position and node creation +function createPosition(line: number, character: number): Position { + return { line, character }; +} + +// Specialized node creation functions using a more concise approach +function createStringNode(value: string, start: Position, end: Position): YamlStringNode { + return { type: 'string', value, start, end }; +} + +function createNumberNode(value: number, start: Position, end: Position): YamlNumberNode { + return { type: 'number', value, start, end }; +} + +function createBooleanNode(value: boolean, start: Position, end: Position): YamlBooleanNode { + return { type: 'boolean', value, start, end }; +} + +function createNullNode(start: Position, end: Position): YamlNullNode { + return { type: 'null', value: null, start, end }; +} + +function createObjectNode(properties: { key: YamlStringNode; value: YamlNode }[], start: Position, end: Position): YamlObjectNode { + return { type: 'object', start, end, properties }; +} + +function createArrayNode(items: YamlNode[], start: Position, end: Position): YamlArrayNode { + return { type: 'array', start, end, items }; +} + +// Utility functions for parsing +function isWhitespace(char: string): boolean { + return char === ' ' || char === '\t'; +} + +// Simplified number validation using regex +function isValidNumber(value: string): boolean { + return /^-?\d*\.?\d+$/.test(value); +} + +// Lexer/Tokenizer for YAML content +class YamlLexer { + private lines: string[]; + private currentLine: number = 0; + private currentChar: number = 0; + + constructor(lines: string[]) { + this.lines = lines; + } + + getCurrentPosition(): Position { + return createPosition(this.currentLine, this.currentChar); + } + + getCurrentLineNumber(): number { + return this.currentLine; + } + + getCurrentCharNumber(): number { + return this.currentChar; + } + + getCurrentLineText(): string { + return this.currentLine < this.lines.length ? this.lines[this.currentLine] : ''; + } + + savePosition(): { line: number; char: number } { + return { line: this.currentLine, char: this.currentChar }; + } + + restorePosition(pos: { line: number; char: number }): void { + this.currentLine = pos.line; + this.currentChar = pos.char; + } + + isAtEnd(): boolean { + return this.currentLine >= this.lines.length; + } + + getCurrentChar(): string { + if (this.isAtEnd() || this.currentChar >= this.lines[this.currentLine].length) { + return ''; + } + return this.lines[this.currentLine][this.currentChar]; + } + + peek(offset: number = 1): string { + const newChar = this.currentChar + offset; + if (this.currentLine >= this.lines.length || newChar >= this.lines[this.currentLine].length) { + return ''; + } + return this.lines[this.currentLine][newChar]; + } + + advance(): string { + const char = this.getCurrentChar(); + if (this.currentChar >= this.lines[this.currentLine].length && this.currentLine < this.lines.length - 1) { + this.currentLine++; + this.currentChar = 0; + } else { + this.currentChar++; + } + return char; + } + + advanceLine(): void { + this.currentLine++; + this.currentChar = 0; + } + + skipWhitespace(): void { + while (!this.isAtEnd() && this.currentChar < this.lines[this.currentLine].length && isWhitespace(this.getCurrentChar())) { + this.advance(); + } + } + + skipToEndOfLine(): void { + this.currentChar = this.lines[this.currentLine].length; + } + + getIndentation(): number { + if (this.isAtEnd()) { + return 0; + } + let indent = 0; + for (let i = 0; i < this.lines[this.currentLine].length; i++) { + if (this.lines[this.currentLine][i] === ' ') { + indent++; + } else if (this.lines[this.currentLine][i] === '\t') { + indent += 4; // Treat tab as 4 spaces + } else { + break; + } + } + return indent; + } + + moveToNextNonEmptyLine(): void { + while (this.currentLine < this.lines.length) { + // First check current line from current position + if (this.currentChar < this.lines[this.currentLine].length) { + const remainingLine = this.lines[this.currentLine].substring(this.currentChar).trim(); + if (remainingLine.length > 0 && !remainingLine.startsWith('#')) { + this.skipWhitespace(); + return; + } + } + + // Move to next line and check from beginning + this.currentLine++; + this.currentChar = 0; + + if (this.currentLine < this.lines.length) { + const line = this.lines[this.currentLine].trim(); + if (line.length > 0 && !line.startsWith('#')) { + this.skipWhitespace(); + return; + } + } + } + } +} + +// Parser class for handling YAML parsing +class YamlParser { + private lexer: YamlLexer; + private errors: YamlParseError[]; + private options: ParseOptions; + // Track nesting level of flow (inline) collections '[' ']' '{' '}' + private flowLevel: number = 0; + + constructor(lines: string[], errors: YamlParseError[], options: ParseOptions) { + this.lexer = new YamlLexer(lines); + this.errors = errors; + this.options = options; + } + + addError(message: string, code: string, start: Position, end: Position): void { + this.errors.push({ message, code, start, end }); + } + + parseValue(expectedIndent?: number): YamlNode { + this.lexer.skipWhitespace(); + + if (this.lexer.isAtEnd()) { + const pos = this.lexer.getCurrentPosition(); + return createStringNode('', pos, pos); + } + + const char = this.lexer.getCurrentChar(); + + // Handle quoted strings + if (char === '"' || char === `'`) { + return this.parseQuotedString(char); + } + + // Handle inline arrays + if (char === '[') { + return this.parseInlineArray(); + } + + // Handle inline objects + if (char === '{') { + return this.parseInlineObject(); + } + + // Handle unquoted values + return this.parseUnquotedValue(); + } + + parseQuotedString(quote: string): YamlNode { + const start = this.lexer.getCurrentPosition(); + this.lexer.advance(); // Skip opening quote + + let value = ''; + while (!this.lexer.isAtEnd() && this.lexer.getCurrentChar() !== '' && this.lexer.getCurrentChar() !== quote) { + value += this.lexer.advance(); + } + + if (this.lexer.getCurrentChar() === quote) { + this.lexer.advance(); // Skip closing quote + } + + const end = this.lexer.getCurrentPosition(); + return createStringNode(value, start, end); + } + + parseUnquotedValue(): YamlNode { + const start = this.lexer.getCurrentPosition(); + let value = ''; + let endPos = start; + + // Helper function to check for value terminators + const isTerminator = (char: string): boolean => { + if (char === '#') { return true; } + // Comma, ']' and '}' only terminate inside flow collections + if (this.flowLevel > 0 && (char === ',' || char === ']' || char === '}')) { return true; } + return false; + }; + + // Handle opening quote that might not be closed + const firstChar = this.lexer.getCurrentChar(); + if (firstChar === '"' || firstChar === `'`) { + value += this.lexer.advance(); + endPos = this.lexer.getCurrentPosition(); + while (!this.lexer.isAtEnd() && this.lexer.getCurrentChar() !== '') { + const char = this.lexer.getCurrentChar(); + if (char === firstChar || isTerminator(char)) { + break; + } + value += this.lexer.advance(); + endPos = this.lexer.getCurrentPosition(); + } + } else { + while (!this.lexer.isAtEnd() && this.lexer.getCurrentChar() !== '') { + const char = this.lexer.getCurrentChar(); + if (isTerminator(char)) { + break; + } + value += this.lexer.advance(); + endPos = this.lexer.getCurrentPosition(); + } + } + const trimmed = value.trimEnd(); + const diff = value.length - trimmed.length; + if (diff) { + endPos = createPosition(start.line, endPos.character - diff); + } + const finalValue = (firstChar === '"' || firstChar === `'`) ? trimmed.substring(1) : trimmed; + return this.createValueNode(finalValue, start, endPos); + } + + private createValueNode(value: string, start: Position, end: Position): YamlNode { + if (value === '') { + return createStringNode('', start, start); + } + + // Boolean values + if (value === 'true') { + return createBooleanNode(true, start, end); + } + if (value === 'false') { + return createBooleanNode(false, start, end); + } + + // Null values + if (value === 'null' || value === '~') { + return createNullNode(start, end); + } + + // Number values + const numberValue = Number(value); + if (!isNaN(numberValue) && isFinite(numberValue) && isValidNumber(value)) { + return createNumberNode(numberValue, start, end); + } + + // Default to string + return createStringNode(value, start, end); + } + + parseInlineArray(): YamlArrayNode { + const start = this.lexer.getCurrentPosition(); + this.lexer.advance(); // Skip '[' + this.flowLevel++; + + const items: YamlNode[] = []; + + while (!this.lexer.isAtEnd()) { + this.lexer.skipWhitespace(); + + // Handle end of array + if (this.lexer.getCurrentChar() === ']') { + this.lexer.advance(); + break; + } + + // Handle end of line - continue to next line for multi-line arrays + if (this.lexer.getCurrentChar() === '') { + this.lexer.advanceLine(); + continue; + } + + // Parse array item + const item = this.parseValue(); + items.push(item); + + this.lexer.skipWhitespace(); + + // Handle comma separator + if (this.lexer.getCurrentChar() === ',') { + this.lexer.advance(); + } + } + + const end = this.lexer.getCurrentPosition(); + this.flowLevel--; + return createArrayNode(items, start, end); + } + + parseInlineObject(): YamlObjectNode { + const start = this.lexer.getCurrentPosition(); + this.lexer.advance(); // Skip '{' + this.flowLevel++; + + const properties: { key: YamlStringNode; value: YamlNode }[] = []; + + while (!this.lexer.isAtEnd()) { + this.lexer.skipWhitespace(); + + // Handle end of object + if (this.lexer.getCurrentChar() === '}') { + this.lexer.advance(); + break; + } + + // Parse key - read until colon + const keyStart = this.lexer.getCurrentPosition(); + let keyValue = ''; + + // Handle quoted keys + if (this.lexer.getCurrentChar() === '"' || this.lexer.getCurrentChar() === `'`) { + const quote = this.lexer.getCurrentChar(); + this.lexer.advance(); // Skip opening quote + + while (!this.lexer.isAtEnd() && this.lexer.getCurrentChar() !== '' && this.lexer.getCurrentChar() !== quote) { + keyValue += this.lexer.advance(); + } + + if (this.lexer.getCurrentChar() === quote) { + this.lexer.advance(); // Skip closing quote + } + } else { + // Handle unquoted keys - read until colon + while (!this.lexer.isAtEnd() && this.lexer.getCurrentChar() !== '' && this.lexer.getCurrentChar() !== ':') { + keyValue += this.lexer.advance(); + } + } + + keyValue = keyValue.trim(); + const keyEnd = this.lexer.getCurrentPosition(); + const key = createStringNode(keyValue, keyStart, keyEnd); + + this.lexer.skipWhitespace(); + + // Expect colon + if (this.lexer.getCurrentChar() === ':') { + this.lexer.advance(); + } + + this.lexer.skipWhitespace(); + + // Parse value + const value = this.parseValue(); + + properties.push({ key, value }); + + this.lexer.skipWhitespace(); + + // Handle comma separator + if (this.lexer.getCurrentChar() === ',') { + this.lexer.advance(); + } + } + + const end = this.lexer.getCurrentPosition(); + this.flowLevel--; + return createObjectNode(properties, start, end); + } + + parseBlockArray(baseIndent: number): YamlArrayNode { + const start = this.lexer.getCurrentPosition(); + const items: YamlNode[] = []; + + while (!this.lexer.isAtEnd()) { + this.lexer.moveToNextNonEmptyLine(); + + if (this.lexer.isAtEnd()) { + break; + } + + const currentIndent = this.lexer.getIndentation(); + + // If indentation is less than expected, we're done with this array + if (currentIndent < baseIndent) { + break; + } + + this.lexer.skipWhitespace(); + + // Check for array item marker + if (this.lexer.getCurrentChar() === '-') { + this.lexer.advance(); // Skip '-' + this.lexer.skipWhitespace(); + + const itemStart = this.lexer.getCurrentPosition(); + + // Check if this is a nested structure + if (this.lexer.getCurrentChar() === '' || this.lexer.getCurrentChar() === '#') { + // Empty item - check if next lines form a nested structure + this.lexer.advanceLine(); + + if (!this.lexer.isAtEnd()) { + const nextIndent = this.lexer.getIndentation(); + + if (nextIndent > currentIndent) { + // Check if the next line starts with a dash (nested array) or has properties (nested object) + this.lexer.skipWhitespace(); + if (this.lexer.getCurrentChar() === '-') { + // It's a nested array + const nestedArray = this.parseBlockArray(nextIndent); + items.push(nestedArray); + } else { + // Check if it looks like an object property (has a colon) + const currentLine = this.lexer.getCurrentLineText(); + const currentPos = this.lexer.getCurrentCharNumber(); + const remainingLine = currentLine.substring(currentPos); + + if (remainingLine.includes(':') && !remainingLine.trim().startsWith('#')) { + // It's a nested object + const nestedObject = this.parseBlockObject(nextIndent, this.lexer.getCurrentCharNumber()); + items.push(nestedObject); + } else { + // Not a nested structure, create empty string + items.push(createStringNode('', itemStart, itemStart)); + } + } + } else { + // No nested content, empty item + items.push(createStringNode('', itemStart, itemStart)); + } + } else { + // End of input, empty item + items.push(createStringNode('', itemStart, itemStart)); + } + } else { + // Parse the item value + // Check if this is a multi-line object by looking for a colon and checking next lines + const currentLine = this.lexer.getCurrentLineText(); + const currentPos = this.lexer.getCurrentCharNumber(); + const remainingLine = currentLine.substring(currentPos); + + // Check if there's a colon on this line (indicating object properties) + const hasColon = remainingLine.includes(':'); + + if (hasColon) { + // Any line with a colon should be treated as an object + // Parse as an object with the current item's indentation as the base + const item = this.parseBlockObject(itemStart.character, itemStart.character); + items.push(item); + } else { + // No colon, parse as regular value + const item = this.parseValue(); + items.push(item); + + // Skip to end of line + while (!this.lexer.isAtEnd() && this.lexer.getCurrentChar() !== '' && this.lexer.getCurrentChar() !== '#') { + this.lexer.advance(); + } + this.lexer.advanceLine(); + } + } + } else { + // No dash found at expected indent level, break + break; + } + } + + // Calculate end position based on the last item + let end = start; + if (items.length > 0) { + const lastItem = items[items.length - 1]; + end = lastItem.end; + } else { + // If no items, end is right after the start + end = createPosition(start.line, start.character + 1); + } + + return createArrayNode(items, start, end); + } + + parseBlockObject(baseIndent: number, baseCharPosition?: number): YamlObjectNode { + const start = this.lexer.getCurrentPosition(); + const properties: { key: YamlStringNode; value: YamlNode }[] = []; + const localKeysSeen = new Set(); + + // For parsing from current position (inline object parsing) + const fromCurrentPosition = baseCharPosition !== undefined; + let firstIteration = true; + + while (!this.lexer.isAtEnd()) { + if (!firstIteration || !fromCurrentPosition) { + this.lexer.moveToNextNonEmptyLine(); + } + firstIteration = false; + + if (this.lexer.isAtEnd()) { + break; + } + + const currentIndent = this.lexer.getIndentation(); + + if (fromCurrentPosition) { + // For current position parsing, check character position alignment + this.lexer.skipWhitespace(); + const currentCharPosition = this.lexer.getCurrentCharNumber(); + + if (currentCharPosition < baseCharPosition) { + break; + } + } else { + // For normal block parsing, check indentation level + if (currentIndent < baseIndent) { + break; + } + + // Check for incorrect indentation + if (currentIndent > baseIndent) { + const lineStart = createPosition(this.lexer.getCurrentLineNumber(), 0); + const lineEnd = createPosition(this.lexer.getCurrentLineNumber(), this.lexer.getCurrentLineText().length); + this.addError('Unexpected indentation', 'indentation', lineStart, lineEnd); + + // Try to recover by treating it as a property anyway + this.lexer.skipWhitespace(); + } else { + this.lexer.skipWhitespace(); + } + } + + // Parse key + const keyStart = this.lexer.getCurrentPosition(); + let keyValue = ''; + + while (!this.lexer.isAtEnd() && this.lexer.getCurrentChar() !== '' && this.lexer.getCurrentChar() !== ':') { + keyValue += this.lexer.advance(); + } + + keyValue = keyValue.trim(); + const keyEnd = this.lexer.getCurrentPosition(); + const key = createStringNode(keyValue, keyStart, keyEnd); + + // Check for duplicate keys + if (!this.options.allowDuplicateKeys && localKeysSeen.has(keyValue)) { + this.addError(`Duplicate key '${keyValue}'`, 'duplicateKey', keyStart, keyEnd); + } + localKeysSeen.add(keyValue); + + // Expect colon + if (this.lexer.getCurrentChar() === ':') { + this.lexer.advance(); + } + + this.lexer.skipWhitespace(); + + // Determine if value is on same line or next line(s) + let value: YamlNode; + const valueStart = this.lexer.getCurrentPosition(); + + if (this.lexer.getCurrentChar() === '' || this.lexer.getCurrentChar() === '#') { + // Value is on next line(s) or empty + this.lexer.advanceLine(); + + // Check next line for nested content + if (!this.lexer.isAtEnd()) { + const nextIndent = this.lexer.getIndentation(); + + if (nextIndent > currentIndent) { + // Nested content - determine if it's an object or array + this.lexer.skipWhitespace(); + + if (this.lexer.getCurrentChar() === '-') { + value = this.parseBlockArray(nextIndent); + } else { + value = this.parseBlockObject(nextIndent); + } + } else if (!fromCurrentPosition && nextIndent === currentIndent) { + // Same indentation level - check if it's an array item + this.lexer.skipWhitespace(); + + if (this.lexer.getCurrentChar() === '-') { + value = this.parseBlockArray(currentIndent); + } else { + value = createStringNode('', valueStart, valueStart); + } + } else { + value = createStringNode('', valueStart, valueStart); + } + } else { + value = createStringNode('', valueStart, valueStart); + } + } else { + // Value is on the same line + value = this.parseValue(); + + // Skip any remaining content on this line (comments, etc.) + while (!this.lexer.isAtEnd() && this.lexer.getCurrentChar() !== '' && this.lexer.getCurrentChar() !== '#') { + if (isWhitespace(this.lexer.getCurrentChar())) { + this.lexer.advance(); + } else { + break; + } + } + + // Skip to end of line if we hit a comment + if (this.lexer.getCurrentChar() === '#') { + this.lexer.skipToEndOfLine(); + } + + // Move to next line for next iteration + if (!this.lexer.isAtEnd() && this.lexer.getCurrentChar() === '') { + this.lexer.advanceLine(); + } + } + + properties.push({ key, value }); + } + + // Calculate the end position based on the last property + let end = start; + if (properties.length > 0) { + const lastProperty = properties[properties.length - 1]; + end = lastProperty.value.end; + } + + return createObjectNode(properties, start, end); + } + + parse(): YamlNode | undefined { + if (this.lexer.isAtEnd()) { + return undefined; + } + + this.lexer.moveToNextNonEmptyLine(); + + if (this.lexer.isAtEnd()) { + return undefined; + } + + // Determine the root structure type + this.lexer.skipWhitespace(); + + if (this.lexer.getCurrentChar() === '-') { + // Check if this is an array item or a negative number + // Look at the character after the dash + const nextChar = this.lexer.peek(); + if (nextChar === ' ' || nextChar === '\t' || nextChar === '' || nextChar === '#') { + // It's an array item (dash followed by whitespace/end/comment) + return this.parseBlockArray(0); + } else { + // It's likely a negative number or other value, treat as single value + return this.parseValue(); + } + } else if (this.lexer.getCurrentChar() === '[') { + // Root is an inline array + return this.parseInlineArray(); + } else if (this.lexer.getCurrentChar() === '{') { + // Root is an inline object + return this.parseInlineObject(); + } else { + // Check if this looks like a key-value pair by looking for a colon + // For single values, there shouldn't be a colon + const currentLine = this.lexer.getCurrentLineText(); + const currentPos = this.lexer.getCurrentCharNumber(); + const remainingLine = currentLine.substring(currentPos); + + // Check if there's a colon that's not inside quotes + let hasColon = false; + let inQuotes = false; + let quoteChar = ''; + + for (let i = 0; i < remainingLine.length; i++) { + const char = remainingLine[i]; + + if (!inQuotes && (char === '"' || char === `'`)) { + inQuotes = true; + quoteChar = char; + } else if (inQuotes && char === quoteChar) { + inQuotes = false; + quoteChar = ''; + } else if (!inQuotes && char === ':') { + hasColon = true; + break; + } else if (!inQuotes && char === '#') { + // Comment starts, stop looking + break; + } + } + + if (hasColon) { + // Root is an object + return this.parseBlockObject(0); + } else { + // Root is a single value + return this.parseValue(); + } + } + } +} + + diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index f1174905b02..785f69925fa 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -256,6 +256,27 @@ suite('MarkdownRenderer', () => { assert.strictEqual(result.innerHTML, `

command1 command2

`); }); + test('Should remove relative links if there is no base url', () => { + const md = new MarkdownString(`[text](./foo) bar`, { + isTrusted: true, + supportHtml: true, + }); + + const result = store.add(renderMarkdown(md)).element; + assert.strictEqual(result.innerHTML, `

text bar

`); + }); + + test('Should support relative links if baseurl is set', () => { + const md = new MarkdownString(`[text](./foo) bar`, { + isTrusted: true, + supportHtml: true, + }); + md.baseUri = URI.parse('https://example.com/path/'); + + const result = store.add(renderMarkdown(md)).element; + assert.strictEqual(result.innerHTML, `

text bar

`); + }); + suite('PlaintextMarkdownRender', () => { test('test code, blockquote, heading, list, listitem, paragraph, table, tablerow, tablecell, strong, em, br, del, text are rendered plaintext', () => { diff --git a/src/vs/base/test/common/cancellation.test.ts b/src/vs/base/test/common/cancellation.test.ts index 38ed33f8d62..7e5ad1060fd 100644 --- a/src/vs/base/test/common/cancellation.test.ts +++ b/src/vs/base/test/common/cancellation.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { CancellationToken, CancellationTokenSource } from '../../common/cancellation.js'; +import { CancellationToken, CancellationTokenSource, CancellationTokenPool } from '../../common/cancellation.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; suite('CancellationToken', function () { @@ -125,3 +125,155 @@ suite('CancellationToken', function () { parent.dispose(); }); }); + +suite('CancellationTokenPool', function () { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('empty pool token is not cancelled', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + assert.strictEqual(pool.token.isCancellationRequested, false); + }); + + test('pool token cancels when all tokens are cancelled', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source1 = new CancellationTokenSource(); + const source2 = new CancellationTokenSource(); + const source3 = new CancellationTokenSource(); + + pool.add(source1.token); + pool.add(source2.token); + pool.add(source3.token); + + assert.strictEqual(pool.token.isCancellationRequested, false); + + source1.cancel(); + assert.strictEqual(pool.token.isCancellationRequested, false); + + source2.cancel(); + assert.strictEqual(pool.token.isCancellationRequested, false); + + source3.cancel(); + assert.strictEqual(pool.token.isCancellationRequested, true); + + source1.dispose(); + source2.dispose(); + source3.dispose(); + }); + + test('pool token fires cancellation event when all tokens are cancelled', function () { + return new Promise(resolve => { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source1 = new CancellationTokenSource(); + const source2 = new CancellationTokenSource(); + + pool.add(source1.token); + pool.add(source2.token); + + store.add(pool.token.onCancellationRequested(() => resolve())); + + source1.cancel(); + source2.cancel(); + + source1.dispose(); + source2.dispose(); + }); + }); + + test('adding already cancelled token counts immediately', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source1 = new CancellationTokenSource(); + const source2 = new CancellationTokenSource(); + + source1.cancel(); // Cancel before adding to pool + + pool.add(source1.token); + assert.strictEqual(pool.token.isCancellationRequested, true); // 1 of 1 cancelled, so pool is cancelled + + pool.add(source2.token); // Adding after pool is done should have no effect + assert.strictEqual(pool.token.isCancellationRequested, true); + + source2.cancel(); // This should have no effect since pool is already done + assert.strictEqual(pool.token.isCancellationRequested, true); + + source1.dispose(); + source2.dispose(); + }); + + test('adding single already cancelled token cancels pool immediately', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source = new CancellationTokenSource(); + source.cancel(); + + pool.add(source.token); + assert.strictEqual(pool.token.isCancellationRequested, true); // 1 of 1 cancelled + + source.dispose(); + }); + + test('adding token after pool is done has no effect', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source1 = new CancellationTokenSource(); + const source2 = new CancellationTokenSource(); + + pool.add(source1.token); + source1.cancel(); // Pool should be done now + + assert.strictEqual(pool.token.isCancellationRequested, true); + + // Adding another token should have no effect + pool.add(source2.token); + source2.cancel(); + + assert.strictEqual(pool.token.isCancellationRequested, true); + + source1.dispose(); + source2.dispose(); + }); + + test('single token pool behaviour', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source = new CancellationTokenSource(); + pool.add(source.token); + + assert.strictEqual(pool.token.isCancellationRequested, false); + + source.cancel(); + assert.strictEqual(pool.token.isCancellationRequested, true); + + source.dispose(); + }); + + test('pool with only cancelled tokens', function () { + const pool = new CancellationTokenPool(); + store.add(pool); + + const source1 = new CancellationTokenSource(); + const source2 = new CancellationTokenSource(); + + source1.cancel(); + source2.cancel(); + + pool.add(source1.token); + pool.add(source2.token); + + assert.strictEqual(pool.token.isCancellationRequested, true); + + source1.dispose(); + source2.dispose(); + }); +}); diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index c081f420c1e..63940c789ff 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -531,12 +531,6 @@ suite('Event', function () { // assert that all events are delivered in order assert.deepStrictEqual(listener2Events, ['e1', 'e2', 'e3']); }); - - test('Cannot read property \'_actual\' of undefined #142204', function () { - const e = ds.add(new Emitter()); - const dispo = e.event(() => { }); - dispo.dispose.call(undefined); // assert that disposable can be called with this - }); }); suite('AsyncEmitter', function () { diff --git a/src/vs/base/test/common/numbers.test.ts b/src/vs/base/test/common/numbers.test.ts index 94c07090585..e21844eea98 100644 --- a/src/vs/base/test/common/numbers.test.ts +++ b/src/vs/base/test/common/numbers.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; -import { isPointWithinTriangle, randomInt } from '../../common/numbers.js'; +import { isPointWithinTriangle } from '../../common/numbers.js'; suite('isPointWithinTriangle', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -25,176 +25,3 @@ suite('isPointWithinTriangle', () => { assert.ok(result); }); }); - -suite('randomInt', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - /** - * Test helper that allows to run a test on the `randomInt()` - * utility with specified `max` and `min` values. - */ - const testRandomIntUtil = (max: number, min: number | undefined, testName: string) => { - suite(testName, () => { - let i = 0; - while (++i < 5) { - test(`should generate random boolean attempt#${i}`, async () => { - let iterations = 100; - while (iterations-- > 0) { - const int = randomInt(max, min); - - assert( - int <= max, - `Expected ${int} to be less than or equal to ${max}.` - ); - assert( - int >= (min ?? 0), - `Expected ${int} to be greater than or equal to ${min ?? 0}.`, - ); - } - }); - } - - test('should include min and max', async () => { - let iterations = 125; - const results = []; - while (iterations-- > 0) { - results.push(randomInt(max, min)); - } - - assert( - results.includes(max), - `Expected ${results} to include ${max}.`, - ); - assert( - results.includes(min ?? 0), - `Expected ${results} to include ${min ?? 0}.`, - ); - }); - }); - }; - - suite('positive numbers', () => { - testRandomIntUtil(4, 2, 'max: 4, min: 2'); - testRandomIntUtil(4, 0, 'max: 4, min: 0'); - testRandomIntUtil(4, undefined, 'max: 4, min: undefined'); - testRandomIntUtil(1, 0, 'max: 0, min: 0'); - }); - - suite('negative numbers', () => { - testRandomIntUtil(-2, -5, 'max: -2, min: -5'); - testRandomIntUtil(0, -5, 'max: 0, min: -5'); - testRandomIntUtil(0, -1, 'max: 0, min: -1'); - }); - - suite('split numbers', () => { - testRandomIntUtil(3, -1, 'max: 3, min: -1'); - testRandomIntUtil(2, -2, 'max: 2, min: -2'); - testRandomIntUtil(1, -3, 'max: 2, min: -2'); - }); - - suite('errors', () => { - test('should throw if "min" is == "max" #1', () => { - assert.throws(() => { - randomInt(200, 200); - }, `"max"(200) param should be greater than "min"(200)."`); - }); - - test('should throw if "min" is == "max" #2', () => { - assert.throws(() => { - randomInt(2, 2); - }, `"max"(2) param should be greater than "min"(2)."`); - }); - - test('should throw if "min" is == "max" #3', () => { - assert.throws(() => { - randomInt(0); - }, `"max"(0) param should be greater than "min"(0)."`); - }); - - test('should throw if "min" is > "max" #1', () => { - assert.throws(() => { - randomInt(2, 3); - }, `"max"(2) param should be greater than "min"(3)."`); - }); - - test('should throw if "min" is > "max" #2', () => { - assert.throws(() => { - randomInt(999, 2000); - }, `"max"(999) param should be greater than "min"(2000)."`); - }); - - test('should throw if "min" is > "max" #3', () => { - assert.throws(() => { - randomInt(0, 1); - }, `"max"(0) param should be greater than "min"(1)."`); - }); - - test('should throw if "min" is > "max" #4', () => { - assert.throws(() => { - randomInt(-5, 2); - }, `"max"(-5) param should be greater than "min"(2)."`); - }); - - test('should throw if "min" is > "max" #5', () => { - assert.throws(() => { - randomInt(-4, 0); - }, `"max"(-4) param should be greater than "min"(0)."`); - }); - - test('should throw if "min" is > "max" #6', () => { - assert.throws(() => { - randomInt(-4); - }, `"max"(-4) param should be greater than "min"(0)."`); - }); - - test('should throw if "max" is `NaN`', () => { - assert.throws(() => { - randomInt(NaN); - }, `"max" param is not a number."`); - }); - - test('should throw if "min" is `NaN`', () => { - assert.throws(() => { - randomInt(4, NaN); - }, `"min" param is not a number."`); - }); - - suite('infinite arguments', () => { - test('should throw if "max" is infinite [Infinity]', () => { - assert.throws(() => { - randomInt(Infinity); - }, `"max" param is not finite."`); - }); - - test('should throw if "max" is infinite [-Infinity]', () => { - assert.throws(() => { - randomInt(-Infinity); - }, `"max" param is not finite."`); - }); - - test('should throw if "max" is infinite [+Infinity]', () => { - assert.throws(() => { - randomInt(+Infinity); - }, `"max" param is not finite."`); - }); - - test('should throw if "min" is infinite [Infinity]', () => { - assert.throws(() => { - randomInt(Infinity, Infinity); - }, `"max" param is not finite."`); - }); - - test('should throw if "min" is infinite [-Infinity]', () => { - assert.throws(() => { - randomInt(Infinity, -Infinity); - }, `"max" param is not finite."`); - }); - - test('should throw if "min" is infinite [+Infinity]', () => { - assert.throws(() => { - randomInt(Infinity, +Infinity); - }, `"max" param is not finite."`); - }); - }); - }); -}); diff --git a/src/vs/base/test/common/oauth.test.ts b/src/vs/base/test/common/oauth.test.ts index 78de7c02649..f0cf9cd766e 100644 --- a/src/vs/base/test/common/oauth.test.ts +++ b/src/vs/base/test/common/oauth.test.ts @@ -473,9 +473,7 @@ suite('OAuth', () => { assert.deepStrictEqual(requestBody.redirect_uris, [ 'https://insiders.vscode.dev/redirect', 'https://vscode.dev/redirect', - 'http://localhost/', 'http://127.0.0.1/', - `http://localhost:${DEFAULT_AUTH_FLOW_PORT}/`, `http://127.0.0.1:${DEFAULT_AUTH_FLOW_PORT}/` ]); diff --git a/src/vs/base/test/common/yaml.test.ts b/src/vs/base/test/common/yaml.test.ts new file mode 100644 index 00000000000..be4ced9d65a --- /dev/null +++ b/src/vs/base/test/common/yaml.test.ts @@ -0,0 +1,1084 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { deepStrictEqual, strictEqual, ok } from 'assert'; +import { parse, ParseOptions, YamlParseError, Position, YamlNode } from '../../common/yaml.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; + +function assertValidParse(input: string[], expected: YamlNode, expectedErrors: YamlParseError[], options?: ParseOptions): void { + const errors: YamlParseError[] = []; + const text = input.join('\n'); + const actual1 = parse(text, errors, options); + deepStrictEqual(actual1, expected); + deepStrictEqual(errors, expectedErrors); +} + +function pos(line: number, character: number): Position { + return { line, character }; +} + +suite('YAML Parser', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('scalars', () => { + + test('numbers', () => { + assertValidParse(['1'], { type: 'number', start: pos(0, 0), end: pos(0, 1), value: 1 }, []); + assertValidParse(['1.234'], { type: 'number', start: pos(0, 0), end: pos(0, 5), value: 1.234 }, []); + assertValidParse(['-42'], { type: 'number', start: pos(0, 0), end: pos(0, 3), value: -42 }, []); + }); + + test('boolean', () => { + assertValidParse(['true'], { type: 'boolean', start: pos(0, 0), end: pos(0, 4), value: true }, []); + assertValidParse(['false'], { type: 'boolean', start: pos(0, 0), end: pos(0, 5), value: false }, []); + }); + + test('null', () => { + assertValidParse(['null'], { type: 'null', start: pos(0, 0), end: pos(0, 4), value: null }, []); + assertValidParse(['~'], { type: 'null', start: pos(0, 0), end: pos(0, 1), value: null }, []); + }); + + test('string', () => { + assertValidParse(['A Developer'], { type: 'string', start: pos(0, 0), end: pos(0, 11), value: 'A Developer' }, []); + assertValidParse(['\'A Developer\''], { type: 'string', start: pos(0, 0), end: pos(0, 13), value: 'A Developer' }, []); + assertValidParse(['"A Developer"'], { type: 'string', start: pos(0, 0), end: pos(0, 13), value: 'A Developer' }, []); + assertValidParse(['*.js,*.ts'], { type: 'string', start: pos(0, 0), end: pos(0, 9), value: '*.js,*.ts' }, []); + }); + }); + + suite('objects', () => { + + test('simple properties', () => { + assertValidParse(['name: John Doe'], { + type: 'object', start: pos(0, 0), end: pos(0, 14), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 4), value: 'name' }, + value: { type: 'string', start: pos(0, 6), end: pos(0, 14), value: 'John Doe' } + } + ] + }, []); + assertValidParse(['age: 30'], { + type: 'object', start: pos(0, 0), end: pos(0, 7), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 3), value: 'age' }, + value: { type: 'number', start: pos(0, 5), end: pos(0, 7), value: 30 } + } + ] + }, []); + assertValidParse(['active: true'], { + type: 'object', start: pos(0, 0), end: pos(0, 12), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 6), value: 'active' }, + value: { type: 'boolean', start: pos(0, 8), end: pos(0, 12), value: true } + } + ] + }, []); + assertValidParse(['value: null'], { + type: 'object', start: pos(0, 0), end: pos(0, 11), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 5), value: 'value' }, + value: { type: 'null', start: pos(0, 7), end: pos(0, 11), value: null } + } + ] + }, []); + }); + + test('multiple properties', () => { + assertValidParse( + [ + 'name: John Doe', + 'age: 30' + ], + { + type: 'object', start: pos(0, 0), end: pos(1, 7), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 4), value: 'name' }, + value: { type: 'string', start: pos(0, 6), end: pos(0, 14), value: 'John Doe' } + }, + { + key: { type: 'string', start: pos(1, 0), end: pos(1, 3), value: 'age' }, + value: { type: 'number', start: pos(1, 5), end: pos(1, 7), value: 30 } + } + ] + }, + [] + ); + }); + + test('nested object', () => { + assertValidParse( + [ + 'person:', + ' name: John Doe', + ' age: 30' + ], + { + type: 'object', start: pos(0, 0), end: pos(2, 9), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 6), value: 'person' }, + value: { + type: 'object', start: pos(1, 2), end: pos(2, 9), properties: [ + { + key: { type: 'string', start: pos(1, 2), end: pos(1, 6), value: 'name' }, + value: { type: 'string', start: pos(1, 8), end: pos(1, 16), value: 'John Doe' } + }, + { + key: { type: 'string', start: pos(2, 2), end: pos(2, 5), value: 'age' }, + value: { type: 'number', start: pos(2, 7), end: pos(2, 9), value: 30 } + } + ] + } + } + ] + + }, + [] + ); + }); + + + test('nested objects with address', () => { + assertValidParse( + [ + 'person:', + ' name: John Doe', + ' age: 30', + ' address:', + ' street: 123 Main St', + ' city: Example City' + ], + { + type: 'object', start: pos(0, 0), end: pos(5, 22), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 6), value: 'person' }, + value: { + type: 'object', start: pos(1, 2), end: pos(5, 22), + properties: [ + { + key: { type: 'string', start: pos(1, 2), end: pos(1, 6), value: 'name' }, + value: { type: 'string', start: pos(1, 8), end: pos(1, 16), value: 'John Doe' } + }, + { + key: { type: 'string', start: pos(2, 2), end: pos(2, 5), value: 'age' }, + value: { type: 'number', start: pos(2, 7), end: pos(2, 9), value: 30 } + }, + { + key: { type: 'string', start: pos(3, 2), end: pos(3, 9), value: 'address' }, + value: { + type: 'object', start: pos(4, 4), end: pos(5, 22), properties: [ + { + key: { type: 'string', start: pos(4, 4), end: pos(4, 10), value: 'street' }, + value: { type: 'string', start: pos(4, 12), end: pos(4, 23), value: '123 Main St' } + }, + { + key: { type: 'string', start: pos(5, 4), end: pos(5, 8), value: 'city' }, + value: { type: 'string', start: pos(5, 10), end: pos(5, 22), value: 'Example City' } + } + ] + } + } + ] + } + } + ] + }, + [] + ); + }); + + test('properties without space after colon', () => { + assertValidParse( + ['name:John'], + { + type: 'object', start: pos(0, 0), end: pos(0, 9), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 4), value: 'name' }, + value: { type: 'string', start: pos(0, 5), end: pos(0, 9), value: 'John' } + } + ] + }, + [] + ); + + // Test mixed: some properties with space, some without + assertValidParse( + [ + 'config:', + ' database:', + ' host:localhost', + ' port: 5432', + ' abcde123456:', + ' logger12:admin', + ' memory12: a23123112' + ], + { + type: 'object', start: pos(0, 0), end: pos(6, 25), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 6), value: 'config' }, + value: { + type: 'object', start: pos(1, 2), end: pos(6, 25), properties: [ + { + key: { type: 'string', start: pos(1, 2), end: pos(1, 10), value: 'database' }, + value: { + type: 'object', start: pos(2, 4), end: pos(6, 25), properties: [ + { + key: { type: 'string', start: pos(2, 4), end: pos(2, 8), value: 'host' }, + value: { type: 'string', start: pos(2, 9), end: pos(2, 18), value: 'localhost' } + }, + { + key: { type: 'string', start: pos(3, 4), end: pos(3, 8), value: 'port' }, + value: { type: 'number', start: pos(3, 10), end: pos(3, 14), value: 5432 } + }, + { + key: { type: 'string', start: pos(4, 4), end: pos(4, 15), value: 'abcde123456' }, + value: { + type: 'object', start: pos(5, 6), end: pos(6, 25), properties: [ + { + key: { type: 'string', start: pos(5, 6), end: pos(5, 14), value: 'logger12' }, + value: { type: 'string', start: pos(5, 15), end: pos(5, 20), value: 'admin' } + }, + { + key: { type: 'string', start: pos(6, 6), end: pos(6, 14), value: 'memory12' }, + value: { type: 'string', start: pos(6, 16), end: pos(6, 25), value: 'a23123112' } + } + ] + } + } + ] + } + } + ] + } + } + ] + }, + [] + ); + }); + + test('inline objects', () => { + assertValidParse( + ['{name: John, age: 30}'], + { + type: 'object', start: pos(0, 0), end: pos(0, 21), properties: [ + { + key: { type: 'string', start: pos(0, 1), end: pos(0, 5), value: 'name' }, + value: { type: 'string', start: pos(0, 7), end: pos(0, 11), value: 'John' } + }, + { + key: { type: 'string', start: pos(0, 13), end: pos(0, 16), value: 'age' }, + value: { type: 'number', start: pos(0, 18), end: pos(0, 20), value: 30 } + } + ] + }, + [] + ); + + // Test with different data types + assertValidParse( + ['{active: true, score: 85.5, role: null}'], + { + type: 'object', start: pos(0, 0), end: pos(0, 39), properties: [ + { + key: { type: 'string', start: pos(0, 1), end: pos(0, 7), value: 'active' }, + value: { type: 'boolean', start: pos(0, 9), end: pos(0, 13), value: true } + }, + { + key: { type: 'string', start: pos(0, 15), end: pos(0, 20), value: 'score' }, + value: { type: 'number', start: pos(0, 22), end: pos(0, 26), value: 85.5 } + }, + { + key: { type: 'string', start: pos(0, 28), end: pos(0, 32), value: 'role' }, + value: { type: 'null', start: pos(0, 34), end: pos(0, 38), value: null } + } + ] + }, + [] + ); + + // Test empty inline object + assertValidParse( + ['{}'], + { + type: 'object', start: pos(0, 0), end: pos(0, 2), properties: [] + }, + [] + ); + + // Test inline object with quoted keys and values + assertValidParse( + ['{"name": "John Doe", "age": 30}'], + { + type: 'object', start: pos(0, 0), end: pos(0, 31), properties: [ + { + key: { type: 'string', start: pos(0, 1), end: pos(0, 7), value: 'name' }, + value: { type: 'string', start: pos(0, 9), end: pos(0, 19), value: 'John Doe' } + }, + { + key: { type: 'string', start: pos(0, 21), end: pos(0, 26), value: 'age' }, + value: { type: 'number', start: pos(0, 28), end: pos(0, 30), value: 30 } + } + ] + }, + [] + ); + + // Test inline object without spaces + assertValidParse( + ['{name:John,age:30}'], + { + type: 'object', start: pos(0, 0), end: pos(0, 18), properties: [ + { + key: { type: 'string', start: pos(0, 1), end: pos(0, 5), value: 'name' }, + value: { type: 'string', start: pos(0, 6), end: pos(0, 10), value: 'John' } + }, + { + key: { type: 'string', start: pos(0, 11), end: pos(0, 14), value: 'age' }, + value: { type: 'number', start: pos(0, 15), end: pos(0, 17), value: 30 } + } + ] + }, + [] + ); + }); + + test('special characters in values', () => { + // Test values with special characters + assertValidParse( + [`key: value with \t special chars`], + { + type: 'object', start: pos(0, 0), end: pos(0, 31), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 3), value: 'key' }, + value: { type: 'string', start: pos(0, 5), end: pos(0, 31), value: `value with \t special chars` } + } + ] + }, + [] + ); + }); + + test('various whitespace types', () => { + // Test different types of whitespace + assertValidParse( + [`key:\t \t \t value`], + { + type: 'object', start: pos(0, 0), end: pos(0, 15), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 3), value: 'key' }, + value: { type: 'string', start: pos(0, 10), end: pos(0, 15), value: 'value' } + } + ] + }, + [] + ); + }); + }); + + suite('arrays', () => { + + + test('arrays', () => { + assertValidParse( + [ + '- Boston Red Sox', + '- Detroit Tigers', + '- New York Yankees' + ], + { + type: 'array', start: pos(0, 0), end: pos(2, 18), items: [ + { type: 'string', start: pos(0, 2), end: pos(0, 16), value: 'Boston Red Sox' }, + { type: 'string', start: pos(1, 2), end: pos(1, 16), value: 'Detroit Tigers' }, + { type: 'string', start: pos(2, 2), end: pos(2, 18), value: 'New York Yankees' } + ] + + }, + [] + ); + }); + + + test('inline arrays', () => { + assertValidParse( + ['[Apple, Banana, Cherry]'], + { + type: 'array', start: pos(0, 0), end: pos(0, 23), items: [ + { type: 'string', start: pos(0, 1), end: pos(0, 6), value: 'Apple' }, + { type: 'string', start: pos(0, 8), end: pos(0, 14), value: 'Banana' }, + { type: 'string', start: pos(0, 16), end: pos(0, 22), value: 'Cherry' } + ] + + }, + [] + ); + }); + + test('multi-line inline arrays', () => { + assertValidParse( + [ + '[', + ' geen, ', + ' yello, red]' + ], + { + type: 'array', start: pos(0, 0), end: pos(2, 15), items: [ + { type: 'string', start: pos(1, 4), end: pos(1, 8), value: 'geen' }, + { type: 'string', start: pos(2, 4), end: pos(2, 9), value: 'yello' }, + { type: 'string', start: pos(2, 11), end: pos(2, 14), value: 'red' } + ] + }, + [] + ); + }); + + test('arrays of arrays', () => { + assertValidParse( + [ + '-', + ' - Apple', + ' - Banana', + ' - Cherry' + ], + { + type: 'array', start: pos(0, 0), end: pos(3, 10), items: [ + { + type: 'array', start: pos(1, 2), end: pos(3, 10), items: [ + { type: 'string', start: pos(1, 4), end: pos(1, 9), value: 'Apple' }, + { type: 'string', start: pos(2, 4), end: pos(2, 10), value: 'Banana' }, + { type: 'string', start: pos(3, 4), end: pos(3, 10), value: 'Cherry' } + ] + } + ] + }, + [] + ); + }); + + test('inline arrays of inline arrays', () => { + assertValidParse( + [ + '[', + ' [ee], [ff, gg]', + ']', + ], + { + type: 'array', start: pos(0, 0), end: pos(2, 1), items: [ + { + type: 'array', start: pos(1, 2), end: pos(1, 6), items: [ + { type: 'string', start: pos(1, 3), end: pos(1, 5), value: 'ee' }, + ], + }, + { + type: 'array', start: pos(1, 8), end: pos(1, 16), items: [ + { type: 'string', start: pos(1, 9), end: pos(1, 11), value: 'ff' }, + { type: 'string', start: pos(1, 13), end: pos(1, 15), value: 'gg' }, + ], + } + ] + }, + [] + ); + }); + + test('object with array containing single object', () => { + assertValidParse( + [ + 'items:', + '- name: John', + ' age: 30' + ], + { + type: 'object', start: pos(0, 0), end: pos(2, 9), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 5), value: 'items' }, + value: { + type: 'array', start: pos(1, 0), end: pos(2, 9), items: [ + { + type: 'object', start: pos(1, 2), end: pos(2, 9), properties: [ + { + key: { type: 'string', start: pos(1, 2), end: pos(1, 6), value: 'name' }, + value: { type: 'string', start: pos(1, 8), end: pos(1, 12), value: 'John' } + }, + { + key: { type: 'string', start: pos(2, 2), end: pos(2, 5), value: 'age' }, + value: { type: 'number', start: pos(2, 7), end: pos(2, 9), value: 30 } + } + ] + } + ] + } + } + ] + }, + [] + ); + }); + + test('arrays of objects', () => { + assertValidParse( + [ + '-', + ' name: one', + '- name: two', + '-', + ' name: three' + ], + { + type: 'array', start: pos(0, 0), end: pos(4, 13), items: [ + { + type: 'object', start: pos(1, 2), end: pos(1, 11), properties: [ + { + key: { type: 'string', start: pos(1, 2), end: pos(1, 6), value: 'name' }, + value: { type: 'string', start: pos(1, 8), end: pos(1, 11), value: 'one' } + } + ] + }, + { + type: 'object', start: pos(2, 2), end: pos(2, 11), properties: [ + { + key: { type: 'string', start: pos(2, 2), end: pos(2, 6), value: 'name' }, + value: { type: 'string', start: pos(2, 8), end: pos(2, 11), value: 'two' } + } + ] + }, + { + type: 'object', start: pos(4, 2), end: pos(4, 13), properties: [ + { + key: { type: 'string', start: pos(4, 2), end: pos(4, 6), value: 'name' }, + value: { type: 'string', start: pos(4, 8), end: pos(4, 13), value: 'three' } + } + ] + } + ] + }, + [] + ); + }); + }); + + suite('complex structures', () => { + + test('array of objects', () => { + assertValidParse( + [ + 'products:', + ' - name: Laptop', + ' price: 999.99', + ' in_stock: true', + ' - name: Mouse', + ' price: 25.50', + ' in_stock: false' + ], + { + type: 'object', start: pos(0, 0), end: pos(6, 19), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 8), value: 'products' }, + value: { + type: 'array', start: pos(1, 2), end: pos(6, 19), items: [ + { + type: 'object', start: pos(1, 4), end: pos(3, 18), properties: [ + { + key: { type: 'string', start: pos(1, 4), end: pos(1, 8), value: 'name' }, + value: { type: 'string', start: pos(1, 10), end: pos(1, 16), value: 'Laptop' } + }, + { + key: { type: 'string', start: pos(2, 4), end: pos(2, 9), value: 'price' }, + value: { type: 'number', start: pos(2, 11), end: pos(2, 17), value: 999.99 } + }, + { + key: { type: 'string', start: pos(3, 4), end: pos(3, 12), value: 'in_stock' }, + value: { type: 'boolean', start: pos(3, 14), end: pos(3, 18), value: true } + } + ] + }, + { + type: 'object', start: pos(4, 4), end: pos(6, 19), properties: [ + { + key: { type: 'string', start: pos(4, 4), end: pos(4, 8), value: 'name' }, + value: { type: 'string', start: pos(4, 10), end: pos(4, 15), value: 'Mouse' } + }, + { + key: { type: 'string', start: pos(5, 4), end: pos(5, 9), value: 'price' }, + value: { type: 'number', start: pos(5, 11), end: pos(5, 16), value: 25.50 } + }, + { + key: { type: 'string', start: pos(6, 4), end: pos(6, 12), value: 'in_stock' }, + value: { type: 'boolean', start: pos(6, 14), end: pos(6, 19), value: false } + } + ] + } + ] + } + } + ] + }, + [] + ); + }); + + test('inline array mixed primitives', () => { + assertValidParse( + ['vals: [1, true, null, "str"]'], + { + type: 'object', start: pos(0, 0), end: pos(0, 28), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 4), value: 'vals' }, + value: { + type: 'array', start: pos(0, 6), end: pos(0, 28), items: [ + { type: 'number', start: pos(0, 7), end: pos(0, 8), value: 1 }, + { type: 'boolean', start: pos(0, 10), end: pos(0, 14), value: true }, + { type: 'null', start: pos(0, 16), end: pos(0, 20), value: null }, + { type: 'string', start: pos(0, 22), end: pos(0, 27), value: 'str' } + ] + } + } + ] + }, + [] + ); + }); + + test('mixed inline structures', () => { + assertValidParse( + ['config: {env: "prod", settings: [true, 42], debug: false}'], + { + type: 'object', start: pos(0, 0), end: pos(0, 57), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 6), value: 'config' }, + value: { + type: 'object', start: pos(0, 8), end: pos(0, 57), properties: [ + { + key: { type: 'string', start: pos(0, 9), end: pos(0, 12), value: 'env' }, + value: { type: 'string', start: pos(0, 14), end: pos(0, 20), value: 'prod' } + }, + { + key: { type: 'string', start: pos(0, 22), end: pos(0, 30), value: 'settings' }, + value: { + type: 'array', start: pos(0, 32), end: pos(0, 42), items: [ + { type: 'boolean', start: pos(0, 33), end: pos(0, 37), value: true }, + { type: 'number', start: pos(0, 39), end: pos(0, 41), value: 42 } + ] + } + }, + { + key: { type: 'string', start: pos(0, 44), end: pos(0, 49), value: 'debug' }, + value: { type: 'boolean', start: pos(0, 51), end: pos(0, 56), value: false } + } + ] + } + } + ] + }, + [] + ); + }); + + test('with comments', () => { + assertValidParse( + [ + `# This is a comment`, + 'name: John Doe # inline comment', + 'age: 30' + ], + { + type: 'object', start: pos(1, 0), end: pos(2, 7), properties: [ + { + key: { type: 'string', start: pos(1, 0), end: pos(1, 4), value: 'name' }, + value: { type: 'string', start: pos(1, 6), end: pos(1, 14), value: 'John Doe' } + }, + { + key: { type: 'string', start: pos(2, 0), end: pos(2, 3), value: 'age' }, + value: { type: 'number', start: pos(2, 5), end: pos(2, 7), value: 30 } + } + ] + }, + [] + ); + }); + }); + + suite('edge cases and error handling', () => { + + + // Edge cases + test('duplicate keys error', () => { + assertValidParse( + [ + 'key: 1', + 'key: 2' + ], + { + type: 'object', start: pos(0, 0), end: pos(1, 6), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 3), value: 'key' }, + value: { type: 'number', start: pos(0, 5), end: pos(0, 6), value: 1 } + }, + { + key: { type: 'string', start: pos(1, 0), end: pos(1, 3), value: 'key' }, + value: { type: 'number', start: pos(1, 5), end: pos(1, 6), value: 2 } + } + ] + }, + [ + { + message: "Duplicate key 'key'", + code: 'duplicateKey', + start: pos(1, 0), + end: pos(1, 3) + } + ] + ); + }); + + test('duplicate keys allowed with option', () => { + assertValidParse( + [ + 'key: 1', + 'key: 2' + ], + { + type: 'object', start: pos(0, 0), end: pos(1, 6), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 3), value: 'key' }, + value: { type: 'number', start: pos(0, 5), end: pos(0, 6), value: 1 } + }, + { + key: { type: 'string', start: pos(1, 0), end: pos(1, 3), value: 'key' }, + value: { type: 'number', start: pos(1, 5), end: pos(1, 6), value: 2 } + } + ] + }, + [], + { allowDuplicateKeys: true } + ); + }); + + test('unexpected indentation error with recovery', () => { + // Parser reports error but still captures the over-indented property. + assertValidParse( + [ + 'key: 1', + ' stray: value' + ], + { + type: 'object', start: pos(0, 0), end: pos(1, 16), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 3), value: 'key' }, + value: { type: 'number', start: pos(0, 5), end: pos(0, 6), value: 1 } + }, + { + key: { type: 'string', start: pos(1, 4), end: pos(1, 9), value: 'stray' }, + value: { type: 'string', start: pos(1, 11), end: pos(1, 16), value: 'value' } + } + ] + }, + [ + { + message: 'Unexpected indentation', + code: 'indentation', + start: pos(1, 0), + end: pos(1, 16) + } + ] + ); + }); + + test('empty values and inline empty array', () => { + assertValidParse( + [ + 'empty:', + 'array: []' + ], + { + type: 'object', start: pos(0, 0), end: pos(1, 9), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 5), value: 'empty' }, + value: { type: 'string', start: pos(0, 6), end: pos(0, 6), value: '' } + }, + { + key: { type: 'string', start: pos(1, 0), end: pos(1, 5), value: 'array' }, + value: { type: 'array', start: pos(1, 7), end: pos(1, 9), items: [] } + } + ] + }, + [] + ); + }); + + + + test('nested empty objects', () => { + // Parser should create nodes for both parent and child, with child having empty string value. + assertValidParse( + [ + 'parent:', + ' child:' + ], + { + type: 'object', start: pos(0, 0), end: pos(1, 8), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 6), value: 'parent' }, + value: { + type: 'object', start: pos(1, 2), end: pos(1, 8), properties: [ + { + key: { type: 'string', start: pos(1, 2), end: pos(1, 7), value: 'child' }, + value: { type: 'string', start: pos(1, 8), end: pos(1, 8), value: '' } + } + ] + } + } + ] + }, + [] + ); + }); + + test('empty object with only colons', () => { + // Test object with empty values + assertValidParse( + ["key1:", "key2:", "key3:"], + { + type: 'object', start: pos(0, 0), end: pos(2, 5), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 4), value: 'key1' }, + value: { type: 'string', start: pos(0, 5), end: pos(0, 5), value: '' } + }, + { + key: { type: 'string', start: pos(1, 0), end: pos(1, 4), value: 'key2' }, + value: { type: 'string', start: pos(1, 5), end: pos(1, 5), value: '' } + }, + { + key: { type: 'string', start: pos(2, 0), end: pos(2, 4), value: 'key3' }, + value: { type: 'string', start: pos(2, 5), end: pos(2, 5), value: '' } + } + ] + }, + [] + ); + }); + + test('large input performance', () => { + // Test that large inputs are handled efficiently + const input = Array.from({ length: 1000 }, (_, i) => `key${i}: value${i}`); + const expectedProperties = Array.from({ length: 1000 }, (_, i) => ({ + key: { type: 'string' as const, start: pos(i, 0), end: pos(i, `key${i}`.length), value: `key${i}` }, + value: { type: 'string' as const, start: pos(i, `key${i}: `.length), end: pos(i, `key${i}: value${i}`.length), value: `value${i}` } + })); + + const start = Date.now(); + assertValidParse( + input, + { + type: 'object', + start: pos(0, 0), + end: pos(999, 'key999: value999'.length), + properties: expectedProperties + }, + [] + ); + const duration = Date.now() - start; + + ok(duration < 100, `Parsing took ${duration}ms, expected < 100ms`); + }); + + test('deeply nested structure performance', () => { + // Test that deeply nested structures are handled efficiently + const lines = []; + for (let i = 0; i < 50; i++) { + const indent = ' '.repeat(i); + lines.push(`${indent}level${i}:`); + } + lines.push(' '.repeat(50) + 'deepValue: reached'); + + const start = Date.now(); + const errors: YamlParseError[] = []; + const result = parse(lines.join('\n'), errors); + const duration = Date.now() - start; + + ok(result); + strictEqual(result.type, 'object'); + strictEqual(errors.length, 0); + ok(duration < 100, `Parsing took ${duration}ms, expected < 100ms`); + }); + + test('malformed array with position issues', () => { + // Test malformed arrays that might cause position advancement issues + assertValidParse( + [ + "key: [", + "", + "", + "", + "" + ], + { + type: 'object', start: pos(0, 0), end: pos(5, 0), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 3), value: 'key' }, + value: { type: 'array', start: pos(0, 5), end: pos(5, 0), items: [] } + } + ] + }, + [] + ); + }); + + test('self-referential like structure', () => { + // Test structures that might appear self-referential + assertValidParse( + [ + "a:", + " b:", + " a:", + " b:", + " value: test" + ], + { + type: 'object', start: pos(0, 0), end: pos(4, 19), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 1), value: 'a' }, + value: { + type: 'object', start: pos(1, 2), end: pos(4, 19), properties: [ + { + key: { type: 'string', start: pos(1, 2), end: pos(1, 3), value: 'b' }, + value: { + type: 'object', start: pos(2, 4), end: pos(4, 19), properties: [ + { + key: { type: 'string', start: pos(2, 4), end: pos(2, 5), value: 'a' }, + value: { + type: 'object', start: pos(3, 6), end: pos(4, 19), properties: [ + { + key: { type: 'string', start: pos(3, 6), end: pos(3, 7), value: 'b' }, + value: { + type: 'object', start: pos(4, 8), end: pos(4, 19), properties: [ + { + key: { type: 'string', start: pos(4, 8), end: pos(4, 13), value: 'value' }, + value: { type: 'string', start: pos(4, 15), end: pos(4, 19), value: 'test' } + } + ] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + }, + [] + ); + }); + + test('array with empty lines', () => { + // Test arrays spanning multiple lines with empty lines + assertValidParse( + ["arr: [", "", "item1,", "", "item2", "", "]"], + { + type: 'object', start: pos(0, 0), end: pos(6, 1), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 3), value: 'arr' }, + value: { + type: 'array', start: pos(0, 5), end: pos(6, 1), items: [ + { type: 'string', start: pos(2, 0), end: pos(2, 5), value: 'item1' }, + { type: 'string', start: pos(4, 0), end: pos(4, 5), value: 'item2' } + ] + } + } + ] + }, + [] + ); + }); + + test('whitespace advancement robustness', () => { + // Test that whitespace advancement works correctly + assertValidParse( + [`key: value`], + { + type: 'object', start: pos(0, 0), end: pos(0, 15), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 3), value: 'key' }, + value: { type: 'string', start: pos(0, 10), end: pos(0, 15), value: 'value' } + } + ] + }, + [] + ); + }); + + + test('missing end quote in string values', () => { + // Test unclosed double quote - parser treats it as bare string with quote included + assertValidParse( + ['name: "John'], + { + type: 'object', start: pos(0, 0), end: pos(0, 11), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 4), value: 'name' }, + value: { type: 'string', start: pos(0, 6), end: pos(0, 11), value: 'John' } + } + ] + }, + [] + ); + + // Test unclosed single quote - parser treats it as bare string with quote included + assertValidParse( + ['description: \'Hello world'], + { + type: 'object', start: pos(0, 0), end: pos(0, 25), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 11), value: 'description' }, + value: { type: 'string', start: pos(0, 13), end: pos(0, 25), value: 'Hello world' } + } + ] + }, + [] + ); + + // Test unclosed quote in multi-line context + assertValidParse( + [ + 'data: "incomplete', + 'next: value' + ], + { + type: 'object', start: pos(0, 0), end: pos(1, 11), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 4), value: 'data' }, + value: { type: 'string', start: pos(0, 6), end: pos(0, 17), value: 'incomplete' } + }, + { + key: { type: 'string', start: pos(1, 0), end: pos(1, 4), value: 'next' }, + value: { type: 'string', start: pos(1, 6), end: pos(1, 11), value: 'value' } + } + ] + }, + [] + ); + + // Test properly quoted strings for comparison + assertValidParse( + ['name: "John"'], + { + type: 'object', start: pos(0, 0), end: pos(0, 12), properties: [ + { + key: { type: 'string', start: pos(0, 0), end: pos(0, 4), value: 'name' }, + value: { type: 'string', start: pos(0, 6), end: pos(0, 12), value: 'John' } + } + ] + }, + [] + ); + }); + + + }); + +}); diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts index cc9a23d23b3..a5b5d8c9540 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorViewZones/diffEditorViewZones.ts @@ -395,8 +395,8 @@ export class DiffEditorViewZones extends Disposable { this._register(autorun(reader => { /** @description update scroll modified */ const newScrollTopModified = this._originalScrollTop.read(reader) - - (this._originalScrollOffsetAnimated.get() - this._modifiedScrollOffsetAnimated.read(reader)) - - (this._originalTopPadding.get() - this._modifiedTopPadding.read(reader)); + - (this._originalScrollOffsetAnimated.read(undefined) - this._modifiedScrollOffsetAnimated.read(reader)) + - (this._originalTopPadding.read(undefined) - this._modifiedTopPadding.read(reader)); if (newScrollTopModified !== this._editors.modified.getScrollTop()) { this._editors.modified.setScrollTop(newScrollTopModified, ScrollType.Immediate); } @@ -405,8 +405,8 @@ export class DiffEditorViewZones extends Disposable { this._register(autorun(reader => { /** @description update scroll original */ const newScrollTopOriginal = this._modifiedScrollTop.read(reader) - - (this._modifiedScrollOffsetAnimated.get() - this._originalScrollOffsetAnimated.read(reader)) - - (this._modifiedTopPadding.get() - this._originalTopPadding.read(reader)); + - (this._modifiedScrollOffsetAnimated.read(undefined) - this._originalScrollOffsetAnimated.read(reader)) + - (this._modifiedTopPadding.read(undefined) - this._originalTopPadding.read(reader)); if (newScrollTopOriginal !== this._editors.original.getScrollTop()) { this._editors.original.setScrollTop(newScrollTopOriginal, ScrollType.Immediate); } @@ -419,8 +419,8 @@ export class DiffEditorViewZones extends Disposable { let deltaOrigToMod = 0; if (m) { - const trueTopOriginal = this._editors.original.getTopForLineNumber(m.lineRangeMapping.original.startLineNumber, true) - this._originalTopPadding.get(); - const trueTopModified = this._editors.modified.getTopForLineNumber(m.lineRangeMapping.modified.startLineNumber, true) - this._modifiedTopPadding.get(); + const trueTopOriginal = this._editors.original.getTopForLineNumber(m.lineRangeMapping.original.startLineNumber, true) - this._originalTopPadding.read(undefined); + const trueTopModified = this._editors.modified.getTopForLineNumber(m.lineRangeMapping.modified.startLineNumber, true) - this._modifiedTopPadding.read(undefined); deltaOrigToMod = trueTopModified - trueTopOriginal; } @@ -438,9 +438,9 @@ export class DiffEditorViewZones extends Disposable { } if (this._editors.modified.hasTextFocus()) { - this._originalScrollOffset.set(this._modifiedScrollOffset.get() - deltaOrigToMod, undefined, true); + this._originalScrollOffset.set(this._modifiedScrollOffset.read(undefined) - deltaOrigToMod, undefined, true); } else { - this._modifiedScrollOffset.set(this._originalScrollOffset.get() + deltaOrigToMod, undefined, true); + this._modifiedScrollOffset.set(this._originalScrollOffset.read(undefined) + deltaOrigToMod, undefined, true); } })); } diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts index 660c989c145..c87f163a8d7 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorViewModel.ts @@ -40,7 +40,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo } else { // Reset state transaction(tx => { - for (const r of this._unchangedRegions.get()?.regions || []) { + for (const r of this._unchangedRegions.read(undefined)?.regions || []) { r.collapseAll(tx); } }); @@ -121,7 +121,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo if (touching.length > 1) { didChange = true; const sumLineCount = touching.reduce((sum, r) => sum + r.lineCount, 0); - const r = new UnchangedRegion(touching[0].originalLineNumber, touching[0].modifiedLineNumber, sumLineCount, touching[0].visibleLineCountTop.get(), touching[touching.length - 1].visibleLineCountBottom.get()); + const r = new UnchangedRegion(touching[0].originalLineNumber, touching[0].modifiedLineNumber, sumLineCount, touching[0].visibleLineCountTop.read(undefined), touching[touching.length - 1].visibleLineCountBottom.read(undefined)); newRanges.push(r); } else { newRanges.push(touching[0]); @@ -315,7 +315,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo const state = DiffState.fromDiffResult(result); this._diff.set(state, tx); this._isDiffUpToDate.set(true, tx); - const currentSyncedMovedText = this.movedTextToCompare.get(); + const currentSyncedMovedText = this.movedTextToCompare.read(undefined); this.movedTextToCompare.set(currentSyncedMovedText ? this._lastDiff.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx); }); })); diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 026b0850887..8fd39f2945d 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -294,7 +294,7 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { this._diffModel, this._options, this, - () => isUpdatingViewZones || unchangedRangesFeature.get().isUpdatingHiddenAreas, + () => isUpdatingViewZones || unchangedRangesFeature.read(undefined).isUpdatingHiddenAreas, origViewZoneIdsToIgnore, modViewZoneIdsToIgnore ) diff --git a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts index 6033069a413..97d8e60d70e 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts @@ -260,7 +260,7 @@ class DiffToolBar extends Disposable implements IGutterItemView { overflowBehavior: { maxItems: this._isSmall.read(reader) ? 1 : 3 }, hiddenItemStrategy: HiddenItemStrategy.Ignore, actionRunner: store.add(new ActionRunnerWithContext(() => { - const item = this._item.get(); + const item = this._item.read(undefined); const mapping = item.mapping; return { mapping, diff --git a/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts index 04e0c61e024..a2df0125cd5 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature.ts @@ -244,7 +244,7 @@ export class MovedBlocksLinesFeature extends Disposable { } } - if (movedText !== m.movedTextToCompare.get()) { + if (movedText !== m.movedTextToCompare.read(undefined)) { m.movedTextToCompare.set(undefined, undefined); } m.setActiveMovedText(movedText); diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 9b66d408728..2421f7b7edb 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -4368,7 +4368,7 @@ export interface IInlineSuggestOptions { suppressSuggestions?: boolean; minShowDelay?: number; - + suppressInSnippetMode?: boolean; /** * Does not clear active inline suggestions when the editor loses focus. */ @@ -4433,6 +4433,7 @@ class InlineEditorSuggest extends BaseEditorOption= 2 - && (i < 3 || lineText.charAt(i - 3) !== '\\') - && lineText.charAt(i - 2) === '\\' - && lineText.charAt(i - 1) === 'n' - && lineText.includes('"') - ) { + if (wrapOnEscapedLineFeeds && isEscapedLineBreakAtPosition(lineText, i)) { visibleColumn += breakingColumn; } @@ -496,6 +489,20 @@ function tabCharacterWidth(visibleColumn: number, tabSize: number): number { return (tabSize - (visibleColumn % tabSize)); } +/** + * Checks if the current position in the text should trigger a soft wrap due to escaped line feeds. + * This handles the wrapOnEscapedLineFeeds feature which allows \n sequences in strings to trigger wrapping. + */ +function isEscapedLineBreakAtPosition(lineText: string, i: number): boolean { + return ( + i >= 2 + && (i < 3 || lineText.charAt(i - 3) !== '\\') + && lineText.charAt(i - 2) === '\\' + && lineText.charAt(i - 1) === 'n' + && lineText.includes('"') + ); +} + /** * Kinsoku Shori : Don't break after a leading character, like an open bracket * Kinsoku Shori : Don't break before a trailing character, like a period diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.contribution.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.contribution.ts new file mode 100644 index 00000000000..014050a2119 --- /dev/null +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.contribution.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './floatingMenu.css'; +import { registerEditorContribution, EditorContributionInstantiation } from '../../../browser/editorExtensions.js'; +import { FloatingEditorToolbar } from './floatingMenu.js'; + +registerEditorContribution(FloatingEditorToolbar.ID, FloatingEditorToolbar, EditorContributionInstantiation.AfterFirstRender); diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css new file mode 100644 index 00000000000..ea9e47d9e2a --- /dev/null +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +.floating-menu-overlay-widget { + padding: 0px; + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + border-radius: 2px; + border: 1px solid var(--vscode-contrastBorder); + display: flex; + align-items: center; + z-index: 10; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + overflow: hidden; + + .action-item > .action-label { + padding: 5px; + font-size: 12px; + border-radius: 2px; + } + + .action-item > .action-label.codicon { + color: var(--vscode-button-foreground); + } + + .action-item > .action-label.codicon:not(.separator) { + padding-top: 6px; + padding-bottom: 6px; + } + + .action-item:first-child > .action-label { + padding-left: 7px; + } + + .action-item:last-child > .action-label { + padding-right: 7px; + } + + .action-item .action-label.separator { + background-color: var(--vscode-menu-separatorBackground); + } +} diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts new file mode 100644 index 00000000000..56f8117ef61 --- /dev/null +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { h } from '../../../../base/browser/dom.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, constObservable, observableFromEvent } from '../../../../base/common/observable.js'; +import { MenuEntryActionViewItem } 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 { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { ICodeEditor, OverlayWidgetPositionPreference } from '../../../browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; +import { IEditorContribution } from '../../../common/editorCommon.js'; + +export class FloatingEditorToolbar extends Disposable implements IEditorContribution { + static readonly ID = 'editor.contrib.floatingToolbar'; + + constructor( + editor: ICodeEditor, + @IInstantiationService instantiationService: IInstantiationService, + @IKeybindingService keybindingService: IKeybindingService, + @IMenuService menuService: IMenuService + ) { + super(); + + const editorObs = this._register(observableCodeEditor(editor)); + + const menu = this._register(menuService.createMenu(MenuId.EditorContent, editor.contextKeyService)); + const menuIsEmptyObs = observableFromEvent(this, menu.onDidChange, () => menu.getActions().length === 0); + + this._register(autorun(reader => { + const menuIsEmpty = menuIsEmptyObs.read(reader); + if (menuIsEmpty) { + return; + } + + const container = h('div.floating-menu-overlay-widget'); + + // Set height explicitly to ensure that the floating menu element + // is rendered in the lower right corner at the correct position. + container.root.style.height = '28px'; + + // Toolbar + const toolbar = instantiationService.createInstance(MenuWorkbenchToolBar, container.root, MenuId.EditorContent, { + actionViewItemProvider: (action, options) => { + if (!(action instanceof MenuItemAction)) { + return undefined; + } + + const keybinding = keybindingService.lookupKeybinding(action.id); + if (!keybinding) { + return undefined; + } + + return instantiationService.createInstance(class extends MenuEntryActionViewItem { + protected override updateLabel(): void { + if (this.options.label && this.label) { + this.label.textContent = `${this._commandAction.label} (${keybinding.getLabel()})`; + } + } + }, action, { ...options, keybindingNotRenderedWithLabel: true }); + }, + hiddenItemStrategy: HiddenItemStrategy.Ignore, + menuOptions: { + shouldForwardArgs: true + }, + telemetrySource: 'editor.overlayToolbar', + toolbarOptions: { + primaryGroup: () => true, + useSeparatorsInPrimaryActions: true + }, + }); + + reader.store.add(toolbar); + reader.store.add(autorun(reader => { + const model = editorObs.model.read(reader); + toolbar.context = model?.uri; + })); + + // Overlay widget + reader.store.add(editorObs.createOverlayWidget({ + allowEditorOverflow: false, + domNode: container.root, + minContentWidthInPx: constObservable(0), + position: constObservable({ + preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER + }) + })); + })); + } +} diff --git a/src/vs/editor/contrib/format/browser/format.ts b/src/vs/editor/contrib/format/browser/format.ts index 586663691d1..1d8ec55cf00 100644 --- a/src/vs/editor/contrib/format/browser/format.ts +++ b/src/vs/editor/contrib/format/browser/format.ts @@ -247,6 +247,10 @@ export async function formatDocumentRangesWithProvider( allEdits.push(...minimalEdits); } } + + if (cts.token.isCancellationRequested) { + return true; + } } finally { cts.dispose(); } diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts index 8ce3830a3f2..fab02a670ca 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHints.ts @@ -11,8 +11,7 @@ import { Range } from '../../../common/core/range.js'; import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js'; import { InlayHint, InlayHintList, InlayHintsProvider, Command } from '../../../common/languages.js'; import { ITextModel } from '../../../common/model.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { URI } from '../../../../base/common/uri.js'; +import { createCommandUri } from '../../../../base/common/htmlContent.js'; export class InlayHintAnchor { constructor(readonly range: Range, readonly direction: 'before' | 'after') { } @@ -168,9 +167,5 @@ export class InlayHintsFragments { } export function asCommandLink(command: Command): string { - return URI.from({ - scheme: Schemas.command, - path: command.id, - query: command.arguments && encodeURIComponent(JSON.stringify(command.arguments)) - }).toString(); + return createCommandUri(command.id, ...(command.arguments ?? [])).toString(); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 479d7058565..5508ab07ba8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -162,7 +162,7 @@ export class InlineCompletionsController extends Disposable { if (!model) { return; } const state = model.state.read(reader); if (!state) { return; } - if (!this._focusIsInEditorOrMenu.get()) { return; } + if (!this._focusIsInEditorOrMenu.read(undefined)) { return; } // This controller is in focus, hence reject others. // However if we display a NES that relates to another edit then trigger NES on that related controller @@ -172,7 +172,7 @@ export class InlineCompletionsController extends Disposable { continue; } else if (nextEditUri && isEqual(nextEditUri, ctrl.editor.getModel()?.uri)) { // The next edit in other edito is related to this controller, trigger it. - ctrl.model.get()?.trigger(); + ctrl.model.read(undefined)?.trigger(); } else { ctrl.reject(); } @@ -193,9 +193,9 @@ export class InlineCompletionsController extends Disposable { continue; } // Find the nes from another editor that points to this. - const state = ctrl.model.get()?.state.get(); + const state = ctrl.model.read(undefined)?.state.read(undefined); if (state?.kind === 'inlineEdit' && isEqual(state.nextEditUri, uri)) { - ctrl.model.get()?.stop('automatic'); + ctrl.model.read(undefined)?.stop('automatic'); } } })); @@ -250,16 +250,16 @@ export class InlineCompletionsController extends Disposable { this._register(autorun(reader => { const isFocused = this._focusIsInEditorOrMenu.read(reader); - const model = this.model.get(); + const model = this.model.read(undefined); if (isFocused) { // If this model already has an NES for another editor, then leave as is // Else stop other models. - const state = model?.state?.get(); + const state = model?.state.read(undefined); if (!state || state.kind !== 'inlineEdit' || !state.nextEditUri) { transaction(tx => { for (const ctrl of InlineCompletionsController._instances) { if (ctrl !== this) { - ctrl.model.get()?.stop('automatic', tx); + ctrl.model.read(undefined)?.stop('automatic', tx); } } }); @@ -276,7 +276,7 @@ export class InlineCompletionsController extends Disposable { } if (!model) { return; } - if (model.state.get()?.inlineCompletion?.isFromExplicitRequest && model.inlineEditAvailable.get()) { + if (model.state.read(undefined)?.inlineCompletion?.isFromExplicitRequest && model.inlineEditAvailable.read(undefined)) { // dont hide inline edits on blur when requested explicitly return; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 89608790221..c2c37c5b1bd 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -99,6 +99,8 @@ export class InlineCompletionsModel extends Disposable { private readonly _triggerCommandOnProviderChange; private readonly _minShowDelay; private readonly _showOnSuggestConflict; + private readonly _suppressInSnippetMode; + private readonly _isInSnippetMode; constructor( public readonly textModel: ITextModel, @@ -134,6 +136,10 @@ export class InlineCompletionsModel extends Disposable { this._triggerCommandOnProviderChange = inlineSuggest.map(s => s.triggerCommandOnProviderChange); this._minShowDelay = inlineSuggest.map(s => s.minShowDelay); this._showOnSuggestConflict = inlineSuggest.map(s => s.experimental.showOnSuggestConflict); + this._suppressInSnippetMode = inlineSuggest.map(s => s.suppressInSnippetMode); + + const snippetController = SnippetController2.get(this._editor); + this._isInSnippetMode = snippetController?.isInSnippetObservable ?? constObservable(false); this._typing = this._register(new TypingInterval(this.textModel)); @@ -356,9 +362,9 @@ export class InlineCompletionsModel extends Disposable { this._textModelVersionId.read(reader); // Refetch on text change - const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.get(); + const suggestWidgetInlineCompletions = this._source.suggestWidgetInlineCompletions.read(undefined); let suggestItem = this._selectedSuggestItem.read(reader); - if (this._shouldShowOnSuggestConflict.get()) { + if (this._shouldShowOnSuggestConflict.read(undefined)) { suggestItem = undefined; } if (suggestWidgetInlineCompletions && !suggestItem) { @@ -402,7 +408,7 @@ export class InlineCompletionsModel extends Disposable { includeInlineCompletions: !changeSummary.onlyRequestInlineEdits, includeInlineEdits: this._inlineEditsEnabled.read(reader), requestIssuedDateTime: requestInfo.startTime, - earliestShownDateTime: requestInfo.startTime + (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit || this.inAcceptFlow.get() ? 0 : this._minShowDelay.get()), + earliestShownDateTime: requestInfo.startTime + (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit || this.inAcceptFlow.read(undefined) ? 0 : this._minShowDelay.read(undefined)), }; if (context.triggerKind === InlineCompletionTriggerKind.Automatic && changeSummary.textChange) { @@ -417,10 +423,10 @@ export class InlineCompletionsModel extends Disposable { } } - const itemToPreserveCandidate = this.selectedInlineCompletion.get() ?? this._inlineCompletionItems.get()?.inlineEdit; + const itemToPreserveCandidate = this.selectedInlineCompletion.read(undefined) ?? this._inlineCompletionItems.read(undefined)?.inlineEdit; const itemToPreserve = changeSummary.preserveCurrentCompletion || itemToPreserveCandidate?.forwardStable ? itemToPreserveCandidate : undefined; - const userJumpedToActiveCompletion = this._jumpedToId.map(jumpedTo => !!jumpedTo && jumpedTo === this._inlineCompletionItems.get()?.inlineEdit?.semanticId); + const userJumpedToActiveCompletion = this._jumpedToId.map(jumpedTo => !!jumpedTo && jumpedTo === this._inlineCompletionItems.read(undefined)?.inlineEdit?.semanticId); const providers = changeSummary.provider ? { providers: [changeSummary.provider], label: 'single:' + changeSummary.provider.providerId?.toString() } @@ -607,6 +613,10 @@ export class InlineCompletionsModel extends Disposable { }, (reader) => { const model = this.textModel; + if (this._suppressInSnippetMode.read(reader) && this._isInSnippetMode.read(reader)) { + return undefined; + } + const item = this._inlineCompletionItems.read(reader); const inlineEditResult = item?.inlineEdit; if (inlineEditResult) { 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 adc1e1239fd..d76a3fa99aa 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 @@ -237,7 +237,7 @@ export class InlineEditsGutterIndicator extends Disposable { const idealIconWidth = 22; const minimalIconWidth = 16; // codicon size const iconWidth = (pillRect: Rect) => { - const availableWidth = this._availableWidthForIcon.get()(pillRect.bottom + this._editorObs.editor.getScrollTop()) - gutterViewPortPadding; + const availableWidth = this._availableWidthForIcon.read(undefined)(pillRect.bottom + this._editorObs.editor.getScrollTop()) - gutterViewPortPadding; return Math.max(Math.min(availableWidth, idealIconWidth), minimalIconWidth); }; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts index 15aaec60501..cbfaa632b25 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts @@ -33,7 +33,7 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c const textModel = this._editor.getModel(); if (!textModel) { return undefined; } - const editOffset = model.inlineEditState.get()?.inlineCompletion.updatedEdit; + const editOffset = model.inlineEditState.read(undefined)?.inlineCompletion.updatedEdit; if (!editOffset) { return undefined; } const edits = editOffset.replacements.map(e => { @@ -47,7 +47,7 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c const diffEdits = new TextEdit(edits); const text = new TextModelText(textModel); - return new InlineEditWithChanges(text, diffEdits, model.primaryPosition.get(), inlineEdit.commands, inlineEdit.inlineCompletion); + return new InlineEditWithChanges(text, diffEdits, model.primaryPosition.read(undefined), inlineEdit.commands, inlineEdit.inlineCompletion); }); private readonly _inlineEditModel = derived(this, reader => { diff --git a/src/vs/editor/contrib/middleScroll/browser/middleScrollController.ts b/src/vs/editor/contrib/middleScroll/browser/middleScrollController.ts index c885a89d209..edcdca4e00d 100644 --- a/src/vs/editor/contrib/middleScroll/browser/middleScrollController.ts +++ b/src/vs/editor/contrib/middleScroll/browser/middleScrollController.ts @@ -47,7 +47,7 @@ export class MiddleScrollController extends Disposable implements IEditorContrib ); reader.store.add(this._editor.onMouseDown(e => { - const session = scrollingSession.get(); + const session = scrollingSession.read(undefined); if (session) { scrollingSession.set(undefined, undefined); return; @@ -75,7 +75,7 @@ export class MiddleScrollController extends Disposable implements IEditorContrib }, undefined); store.add(this._editor.onMouseUp(e => { - const session = scrollingSession.get(); + const session = scrollingSession.read(undefined); if (session && session.didScroll) { // Only cancel session on release if the user scrolled during it scrollingSession.set(undefined, undefined); @@ -101,7 +101,7 @@ export class MiddleScrollController extends Disposable implements IEditorContrib const frameDurationMs = curTime - lastTime; lastTime = curTime; - const mouseDelta = session.mouseDeltaAfterThreshold.get(); + const mouseDelta = session.mouseDeltaAfterThreshold.read(undefined); // scroll by mouse delta every 32ms const factor = frameDurationMs / 32; diff --git a/src/vs/editor/contrib/snippet/browser/snippetController2.ts b/src/vs/editor/contrib/snippet/browser/snippetController2.ts index 226efd8aa65..609aa04367b 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetController2.ts @@ -25,6 +25,7 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind import { ILogService } from '../../../../platform/log/common/log.js'; import { ISnippetEdit, SnippetSession } from './snippetSession.js'; import { TextModelEditSource } from '../../../common/textModelEditSource.js'; +import { IObservable, observableValue } from '../../../../base/common/observable.js'; export interface ISnippetInsertOptions { overwriteBefore: number; @@ -60,6 +61,7 @@ export class SnippetController2 implements IEditorContribution { static readonly HasPrevTabstop = new RawContextKey('hasPrevTabstop', false, localize('hasPrevTabstop', "Whether there is a previous tab stop when in snippet mode")); private readonly _inSnippet: IContextKey; + private readonly _inSnippetObservable = observableValue(this, false); private readonly _hasNextTabstop: IContextKey; private readonly _hasPrevTabstop: IContextKey; @@ -84,6 +86,7 @@ export class SnippetController2 implements IEditorContribution { dispose(): void { this._inSnippet.reset(); + this._inSnippetObservable.set(false, undefined); this._hasPrevTabstop.reset(); this._hasNextTabstop.reset(); this._session?.dispose(); @@ -244,6 +247,7 @@ export class SnippetController2 implements IEditorContribution { } this._inSnippet.set(true); + this._inSnippetObservable.set(true, undefined); this._hasPrevTabstop.set(!this._session.isAtFirstPlaceholder); this._hasNextTabstop.set(!this._session.isAtLastPlaceholder); @@ -283,6 +287,7 @@ export class SnippetController2 implements IEditorContribution { cancel(resetSelection: boolean = false): void { this._inSnippet.reset(); + this._inSnippetObservable.set(false, undefined); this._hasPrevTabstop.reset(); this._hasNextTabstop.reset(); this._snippetListener.clear(); @@ -314,6 +319,10 @@ export class SnippetController2 implements IEditorContribution { return Boolean(this._inSnippet.get()); } + get isInSnippetObservable(): IObservable { + return this._inSnippetObservable; + } + getSessionEnclosingRange(): Range | undefined { if (this._session) { return this._session.getEnclosingRange(); diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 59f517623a2..fac1fb1de21 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -873,7 +873,7 @@ export class SuggestWidget implements IDisposable { } const forceRenderingAboveRequiredSpace = 150; - if (height > maxHeightBelow || (this._forceRenderingAbove && availableSpaceAbove > forceRenderingAboveRequiredSpace)) { + if ((height > maxHeightBelow && maxHeightAbove > maxHeightBelow) || (this._forceRenderingAbove && availableSpaceAbove > forceRenderingAboveRequiredSpace)) { this._contentWidget.setPreference(ContentWidgetPositionPreference.ABOVE); this.element.enableSashes(true, true, false, false); maxHeight = maxHeightAbove; diff --git a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts index 270d091293d..b75bc9e61e9 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts @@ -6,7 +6,7 @@ import { RunOnceScheduler } from '../../../../base/common/async.js'; import { CharCode } from '../../../../base/common/charCode.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { createCommandUri, MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import * as platform from '../../../../base/common/platform.js'; import { InvisibleCharacters, isBasicASCII } from '../../../../base/common/strings.js'; @@ -502,7 +502,7 @@ export class UnicodeHighlighterHoverParticipant implements IEditorHoverParticipa }; const adjustSettings = nls.localize('unicodeHighlight.adjustSettings', 'Adjust settings'); - const uri = `command:${ShowExcludeOptions.ID}?${encodeURIComponent(JSON.stringify(adjustSettingsArgs))}`; + const uri = createCommandUri(ShowExcludeOptions.ID, adjustSettingsArgs); const markdown = new MarkdownString('', true) .appendMarkdown(reason) .appendText(' ') diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 1f42a726b7a..916cc40a980 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -63,6 +63,7 @@ import './contrib/wordOperations/browser/wordOperations.js'; import './contrib/wordPartOperations/browser/wordPartOperations.js'; import './contrib/readOnlyMessage/browser/contribution.js'; import './contrib/diffEditorBreadcrumbs/browser/contribution.js'; +import './contrib/floatingMenu/browser/floatingMenu.contribution.js'; // Load up these strings even in VSCode, even if they are not used // in order to get them translated diff --git a/src/vs/editor/standalone/browser/standaloneThemeService.ts b/src/vs/editor/standalone/browser/standaloneThemeService.ts index 8a697f653ff..0ef9470da84 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeService.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeService.ts @@ -263,6 +263,7 @@ export class StandaloneThemeService extends Disposable implements IStandaloneThe })); addMatchMediaChangeListener(mainWindow, '(forced-colors: active)', () => { + // Update theme selection for auto-detecting high contrast this._onOSSchemeChanged(); }); } @@ -399,6 +400,11 @@ export class StandaloneThemeService extends Disposable implements IStandaloneThe const colorMap = this._colorMapOverride || this._theme.tokenTheme.getColorMap(); ruleCollector.addRule(generateTokensCSSForColorMap(colorMap)); + // If the OS has forced-colors active, disable forced color adjustment for + // Monaco editor elements so that VS Code's built-in high contrast themes + // (hc-black / hc-light) are used instead of the OS forcing system colors. + ruleCollector.addRule(`.monaco-editor, .monaco-diff-editor, .monaco-component { forced-color-adjust: none; }`); + this._themeCSS = cssRules.join('\n'); this._updateCSS(); diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index 850e19bc4ad..7861bb31c5b 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -317,4 +317,43 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); assertLineBreaks(factory, 4, 8, '你好1111', WrappingIndent.Same, 'keepAll'); }); + + test('issue wrapOnEscapedLineFeeds: should work correctly after editor resize', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + + // Test text with escaped line feeds - simulates a JSON string with \n + // The \n should trigger a soft wrap when wrapOnEscapedLineFeeds is enabled + const text = '"Short text with\\nescaped newline and more text after"'; + + // First, compute line breaks with wrapOnEscapedLineFeeds enabled at initial width + const initialBreakData = getLineBreakData(factory, 4, 30, 2, WrappingIndent.None, 'normal', true, text, null); + const initialAnnotatedText = toAnnotatedText(text, initialBreakData); + + // Verify the escaped \n triggers a wrap in the initial case + assert.ok(initialAnnotatedText.includes('with\\n'), 'Initial case should wrap at escaped line feeds'); + + // Now simulate editor resize by computing line breaks with different width using previous data + // This triggers createLineBreaksFromPreviousLineBreaks which has the bug + const resizedBreakData = getLineBreakData(factory, 4, 35, 2, WrappingIndent.None, 'normal', true, text, initialBreakData); + const resizedAnnotatedText = toAnnotatedText(text, resizedBreakData); + + // Compute fresh line breaks at the new width (without using previous data) + // This uses createLineBreaks which correctly handles wrapOnEscapedLineFeeds + const freshBreakData = getLineBreakData(factory, 4, 35, 2, WrappingIndent.None, 'normal', true, text, null); + const freshAnnotatedText = toAnnotatedText(text, freshBreakData); + + // Fresh computation should still wrap at escaped line feeds + assert.ok(freshAnnotatedText.includes('with\\n'), 'Fresh computation should wrap at escaped line feeds'); + + // BUG DEMONSTRATION: Incremental computation after resize doesn't handle escaped line feeds + // The two results should be identical, but they're not due to the bug + assert.strictEqual( + resizedAnnotatedText, + freshAnnotatedText, + `Bug: Incremental and fresh computations differ for escaped line feeds.\n` + + `Incremental (resize): ${resizedAnnotatedText}\n` + + `Fresh computation: ${freshAnnotatedText}\n` + + `The incremental path (createLineBreaksFromPreviousLineBreaks) doesn't handle wrapOnEscapedLineFeeds` + ); + }); }); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 03b7dc5427a..c7d6ad95edb 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4774,6 +4774,7 @@ declare namespace monaco.editor { syntaxHighlightingEnabled?: boolean; suppressSuggestions?: boolean; minShowDelay?: number; + suppressInSnippetMode?: boolean; /** * Does not clear active inline suggestions when the editor loses focus. */ diff --git a/src/vs/platform/actionWidget/browser/actionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css index b0327720173..a4771342df3 100644 --- a/src/vs/platform/actionWidget/browser/actionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -154,8 +154,8 @@ /* Action bar */ .action-widget .action-widget-action-bar { - background-color: var(--vscode-editorActionList-background); - border-top: 1px solid var(--vscode-editorHoverWidget-border); + background-color: var(--vscode-menu-background); + border-top: 1px solid var(--vscode-menu-border); margin-top: 2px; } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index be30059545d..4e4ae925681 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -230,6 +230,8 @@ export class MenuId { static readonly ChatCodeBlock = new MenuId('ChatCodeblock'); static readonly ChatCompareBlock = new MenuId('ChatCompareBlock'); static readonly ChatMessageTitle = new MenuId('ChatMessageTitle'); + static readonly ChatHistory = new MenuId('ChatHistory'); + static readonly ChatWelcomeHistoryContext = new MenuId('ChatWelcomeHistoryContext'); static readonly ChatMessageFooter = new MenuId('ChatMessageFooter'); static readonly ChatExecute = new MenuId('ChatExecute'); static readonly ChatExecuteSecondary = new MenuId('ChatExecuteSecondary'); diff --git a/src/vs/platform/assignment/common/assignment.ts b/src/vs/platform/assignment/common/assignment.ts index 6eb1d40f0ed..fe99c39970f 100644 --- a/src/vs/platform/assignment/common/assignment.ts +++ b/src/vs/platform/assignment/common/assignment.ts @@ -63,6 +63,11 @@ export enum Filters { */ ClientId = 'X-MSEdge-ClientId', + /** + * Developer Device Id which can be used as an alternate unit for experimentation. + */ + DeveloperDeviceId = 'X-VSCode-DevDeviceId', + /** * Extension header. */ @@ -90,6 +95,7 @@ export class AssignmentFilterProvider implements IExperimentationFilterProvider private version: string, private appName: string, private machineId: string, + private devDeviceId: string, private targetPopulation: TargetPopulation ) { } @@ -115,6 +121,8 @@ export class AssignmentFilterProvider implements IExperimentationFilterProvider return this.appName; // productService.nameLong case Filters.ClientId: return this.machineId; + case Filters.DeveloperDeviceId: + return this.devDeviceId; case Filters.Language: return platform.language; case Filters.ExtensionName: diff --git a/src/vs/platform/contextkey/browser/contextKeyService.ts b/src/vs/platform/contextkey/browser/contextKeyService.ts index d28bad79662..7f749b68489 100644 --- a/src/vs/platform/contextkey/browser/contextKeyService.ts +++ b/src/vs/platform/contextkey/browser/contextKeyService.ts @@ -5,7 +5,7 @@ import { Emitter, Event, PauseableEmitter } from '../../../base/common/event.js'; import { Iterable } from '../../../base/common/iterator.js'; -import { Disposable, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; import { MarshalledObject } from '../../../base/common/marshalling.js'; import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { cloneAndChange, distinct } from '../../../base/common/objects.js'; @@ -16,6 +16,9 @@ import { CommandsRegistry } from '../../commands/common/commands.js'; import { ConfigurationTarget, IConfigurationService } from '../../configuration/common/configuration.js'; import { ContextKeyExpression, ContextKeyInfo, ContextKeyValue, IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, IScopedContextKeyService, RawContextKey } from '../common/contextkey.js'; import { ServicesAccessor } from '../../instantiation/common/instantiation.js'; +import { InputFocusedContext } from '../common/contextkeys.js'; +import { mainWindow } from '../../../base/browser/window.js'; +import { addDisposableListener, EventType, getActiveWindow, isEditableElement, onDidRegisterWindow, trackFocus } from '../../../base/browser/dom.js'; const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context'; @@ -378,9 +381,12 @@ export class ContextKeyService extends AbstractContextKeyService implements ICon private _lastContextId: number; private readonly _contexts = new Map(); + private inputFocusedContext: IContextKey; + constructor(@IConfigurationService configurationService: IConfigurationService) { super(0); this._lastContextId = 0; + this.inputFocusedContext = InputFocusedContext.bindTo(this); const myContext = this._register(new ConfigAwareContextValuesContainer(this._myContextId, configurationService, this._onDidChangeContext)); this._contexts.set(this._myContextId, myContext); @@ -395,6 +401,44 @@ export class ContextKeyService extends AbstractContextKeyService implements ICon // console.log(lastLoggedValue); // } // }, 2000); + + this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => { + const onFocusDisposables = disposables.add(new MutableDisposable()); + disposables.add(addDisposableListener(window, EventType.FOCUS_IN, () => { + onFocusDisposables.value = new DisposableStore(); + this.updateInputContextKeys(window.document, onFocusDisposables.value); + }, true)); + }, { window: mainWindow, disposables: this._store })); + } + + private updateInputContextKeys(ownerDocument: Document, disposables: DisposableStore): void { + + function activeElementIsInput(): boolean { + return !!ownerDocument.activeElement && isEditableElement(ownerDocument.activeElement); + } + + const isInputFocused = activeElementIsInput(); + this.inputFocusedContext.set(isInputFocused); + + if (isInputFocused) { + const tracker = disposables.add(trackFocus(ownerDocument.activeElement as HTMLElement)); + Event.once(tracker.onDidBlur)(() => { + + // Ensure we are only updating the context key if we are + // still in the same document that we are tracking. This + // fixes a race condition in multi-window setups where + // the blur event arrives in the inactive window overwriting + // the context key of the active window. This is because + // blur events from the focus tracker are emitted with a + // timeout of 0. + + if (getActiveWindow().document === ownerDocument) { + this.inputFocusedContext.set(activeElementIsInput()); + } + + tracker.dispose(); + }, undefined, disposables); + } } public getContextValuesContainer(contextId: number): Context { diff --git a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts index fe483a5a408..7fab8eba7c9 100644 --- a/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts +++ b/src/vs/platform/extensionManagement/common/allowedExtensionsService.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../base/common/lifecycle.js'; -import { URI } from '../../../base/common/uri.js'; import * as nls from '../../../nls.js'; import { IGalleryExtension, AllowedExtensionsConfigKey, IAllowedExtensionsService, AllowedExtensionsConfigValueType } from './extensionManagement.js'; import { ExtensionType, IExtension, TargetPlatform } from '../../extensions/common/extensions.js'; import { IProductService } from '../../product/common/productService.js'; -import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; +import { createCommandUri, IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { isBoolean, isObject, isUndefined } from '../../../base/common/types.js'; import { Emitter } from '../../../base/common/event.js'; @@ -95,7 +94,7 @@ export class AllowedExtensionsService extends Disposable implements IAllowedExte publisherDisplayName = extension.publisherDisplayName?.toLowerCase(); } - const settingsCommandLink = URI.parse(`command:workbench.action.openSettings?${encodeURIComponent(JSON.stringify({ query: `@id:${AllowedExtensionsConfigKey}` }))}`).toString(); + const settingsCommandLink = createCommandUri('workbench.action.openSettings', { query: `@id:${AllowedExtensionsConfigKey}` }).toString(); const extensionValue = this._allowedExtensionsConfigValue[id]; const extensionReason = new MarkdownString(nls.localize('specific extension not allowed', "it is not in the [allowed list]({0})", settingsCommandLink)); if (!isUndefined(extensionValue)) { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index a372982fc75..87fe5f5f2c7 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -1824,10 +1824,12 @@ export abstract class AbstractExtensionGalleryService implements IExtensionGalle } async getExtensionsControlManifest(): Promise { - if (!this.isEnabled()) { + const manifest = await this.extensionGalleryManifestService.getExtensionGalleryManifest(); + if (!manifest) { throw new Error('No extension gallery service configured.'); } + if (!this.extensionsControlUrl) { return { malicious: [], deprecated: {}, search: [], autoUpdate: {} }; } diff --git a/src/vs/platform/extensionManagement/common/implicitActivationEvents.ts b/src/vs/platform/extensionManagement/common/implicitActivationEvents.ts index b8a52b5e9aa..7fdeb16a04b 100644 --- a/src/vs/platform/extensionManagement/common/implicitActivationEvents.ts +++ b/src/vs/platform/extensionManagement/common/implicitActivationEvents.ts @@ -7,7 +7,7 @@ import { onUnexpectedError } from '../../../base/common/errors.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../extensions/common/extensions.js'; export interface IActivationEventsGenerator { - (contributions: T[], result: { push(item: string): void }): void; + (contributions: readonly T[]): Iterable; } export class ImplicitActivationEventsImpl { @@ -73,7 +73,7 @@ export class ImplicitActivationEventsImpl { const contrib = (desc.contributes as any)[extPointName]; const contribArr = Array.isArray(contrib) ? contrib : [contrib]; try { - generator(contribArr, activationEvents); + activationEvents.push(...generator(contribArr)); } catch (err) { onUnexpectedError(err); } diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index 87a9288104d..0683f680cb7 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -417,7 +417,7 @@ function isVersionValid(currentVersion: string, date: ProductDate, requestedVers return true; } -function isStringArray(arr: string[]): boolean { +function isStringArray(arr: readonly string[]): boolean { if (!Array.isArray(arr)) { return false; } diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index bf4bb318095..4c5d20c9b13 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -196,6 +196,7 @@ export interface IToolSetContribution { export interface IMcpCollectionContribution { readonly id: string; readonly label: string; + readonly when?: string; } export interface IExtensionContributions { @@ -299,7 +300,7 @@ export interface IRelaxedExtensionManifest { icon?: string; categories?: string[]; keywords?: string[]; - activationEvents?: string[]; + activationEvents?: readonly string[]; extensionDependencies?: string[]; extensionPack?: string[]; extensionKind?: ExtensionKind | ExtensionKind[]; diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 9a20aa9cc79..54f7802a485 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -62,6 +62,7 @@ const _allApiProposals = { }, chatSessionsProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts', + version: 2 }, chatStatusItem: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts', diff --git a/src/vs/platform/mcp/common/allowedMcpServersService.ts b/src/vs/platform/mcp/common/allowedMcpServersService.ts index 529b3109252..34d423fe4ac 100644 --- a/src/vs/platform/mcp/common/allowedMcpServersService.ts +++ b/src/vs/platform/mcp/common/allowedMcpServersService.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../base/common/lifecycle.js'; -import { URI } from '../../../base/common/uri.js'; import * as nls from '../../../nls.js'; -import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; +import { createCommandUri, IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { Emitter } from '../../../base/common/event.js'; import { IAllowedMcpServersService, IGalleryMcpServer, IInstallableMcpServer, ILocalMcpServer, mcpAccessConfig, McpAccessValue } from './mcpManagement.js'; @@ -34,7 +33,7 @@ export class AllowedMcpServersService extends Disposable implements IAllowedMcpS return true; } - const settingsCommandLink = URI.parse(`command:workbench.action.openSettings?${encodeURIComponent(JSON.stringify({ query: `@id:${mcpAccessConfig}` }))}`).toString(); + const settingsCommandLink = createCommandUri('workbench.action.openSettings', { query: `@id:${mcpAccessConfig}` }).toString(); return new MarkdownString(nls.localize('mcp servers are not allowed', "Model Context Protocol servers are disabled in the Editor. Please check your [settings]({0}).", settingsCommandLink)); } } diff --git a/src/vs/platform/mcp/common/mcpGalleryManifest.ts b/src/vs/platform/mcp/common/mcpGalleryManifest.ts index 759f09c5b86..9c108ab7780 100644 --- a/src/vs/platform/mcp/common/mcpGalleryManifest.ts +++ b/src/vs/platform/mcp/common/mcpGalleryManifest.ts @@ -8,6 +8,7 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; export const enum McpGalleryResourceType { McpServersQueryService = 'McpServersQueryService', + McpServersSearchService = 'McpServersSearchService', McpServerWebUri = 'McpServerWebUriTemplate', McpServerResourceUri = 'McpServerResourceUriTemplate', McpServerNamedResourceUri = 'McpServerNamedResourceUriTemplate', diff --git a/src/vs/platform/mcp/common/mcpGalleryManifestService.ts b/src/vs/platform/mcp/common/mcpGalleryManifestService.ts index 43080ff3ccb..a366e79c1f6 100644 --- a/src/vs/platform/mcp/common/mcpGalleryManifestService.ts +++ b/src/vs/platform/mcp/common/mcpGalleryManifestService.ts @@ -51,6 +51,10 @@ export class McpGalleryManifestService extends Disposable implements IMcpGallery } if (isProductGalleryUrl) { + resources.push({ + id: `${serversUrl}/search`, + type: McpGalleryResourceType.McpServersSearchService + }); resources.push({ id: `${serversUrl}/by-name/{name}`, type: McpGalleryResourceType.McpServerNamedResourceUri diff --git a/src/vs/platform/mcp/common/mcpGalleryService.ts b/src/vs/platform/mcp/common/mcpGalleryService.ts index 7c5017f21d9..500ffa5ca19 100644 --- a/src/vs/platform/mcp/common/mcpGalleryService.ts +++ b/src/vs/platform/mcp/common/mcpGalleryService.ts @@ -29,6 +29,11 @@ interface McpServerDeprecatedRemote { type McpServerRemotes = ReadonlyArray; +interface IRawGalleryMcpServerPackage extends IMcpServerPackage { + readonly registry_name: string; + readonly name: string; +} + interface IRawGalleryServerListMetadata { readonly count: number; readonly total?: number; @@ -44,40 +49,56 @@ interface IMcpRegistryInfo { } interface IGitHubInfo { - readonly 'name': string; - readonly 'name_with_owner': string; - readonly 'is_in_organization'?: boolean; - readonly 'license'?: string; - readonly 'opengraph_image_url'?: string; - readonly 'owner_avatar_url'?: string; - readonly 'primary_language'?: string; - readonly 'primary_language_color'?: string; - readonly 'pushed_at'?: string; - readonly 'stargazer_count'?: number; - readonly 'topics'?: readonly string[]; - readonly 'uses_custom_opengraph_image'?: boolean; -} - -interface IRawGalleryMcpServerMetaData { - readonly 'io.modelcontextprotocol.registry/official'?: IMcpRegistryInfo; - readonly 'x-io.modelcontextprotocol.registry'?: IMcpRegistryInfo; - readonly 'x-publisher'?: Record; - readonly 'io.modelcontextprotocol.registry/publisher-provided'?: Record; - readonly 'x-github'?: IGitHubInfo; - readonly 'github'?: IGitHubInfo; -} - -function isIRawGalleryServersOldResult(obj: any): obj is IRawGalleryServersOldResult { - return obj && Array.isArray(obj.servers) && isIRawGalleryOldMcpServer(obj.servers[0]); + readonly name: string; + readonly name_with_owner: string; + readonly display_name?: string; + readonly is_in_organization?: boolean; + readonly license?: string; + readonly opengraph_image_url?: string; + readonly owner_avatar_url?: string; + readonly primary_language?: string; + readonly primary_language_color?: string; + readonly pushed_at?: string; + readonly stargazer_count?: number; + readonly topics?: readonly string[]; + readonly uses_custom_opengraph_image?: boolean; } function isIRawGalleryOldMcpServer(obj: any): obj is IRawGalleryOldMcpServer { return obj && obj.server !== undefined; } -interface IRawGalleryServersResult { - readonly metadata?: IRawGalleryServerListMetadata; - readonly servers: readonly IRawGalleryMcpServer[]; +interface IRawGalleryOldMcpServer { + readonly server: { + readonly id: string; + readonly name: string; + readonly description: string; + readonly version_detail: { + readonly version: string; + readonly release_date: string; + readonly is_latest: boolean; + }; + readonly status?: GalleryMcpServerStatus; + readonly repository?: { + readonly source: string; + readonly url: string; + readonly id?: string; + readonly subfolder?: string; + readonly readme?: string; + }; + readonly created_at: string; + readonly updated_at: string; + readonly packages?: readonly IRawGalleryMcpServerPackage[]; + readonly remotes?: McpServerRemotes; + }; + readonly 'x-io.modelcontextprotocol.registry': { + readonly id: string; + readonly is_latest: boolean; + readonly published_at: string; + readonly updated_at: string; + readonly release_date?: string; + }; + readonly 'x-github'?: IGitHubInfo; } interface IRawGalleryServersOldResult { @@ -85,40 +106,94 @@ interface IRawGalleryServersOldResult { readonly servers: readonly IRawGalleryOldMcpServer[]; } -interface IRawGalleryOldMcpServer extends IRawGalleryMcpServerMetaData { - readonly server: IRawGalleryMcpServerDetail; +const enum McpServerSchemaVersion { + v2025_01_09 = 'https://static.modelcontextprotocol.io/schemas/2025-09-01/server.schema.json', + v2025_07_09 = 'https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json', + v2025_09_16 = 'https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json', } -interface IRawGalleryMcpServer extends IRawGalleryMcpServerDetail { - readonly _meta?: IRawGalleryMcpServerMetaData; -} - -interface IRawGalleryMcpServerPackage extends IMcpServerPackage { - readonly registry_name: string; - readonly name: string; -} - -interface IRawGalleryMcpServerDetail { - readonly id: string; +interface IRawGalleryMcpServer_2025_01_09 { + readonly $schema?: McpServerSchemaVersion.v2025_01_09; readonly name: string; readonly description: string; - readonly version_detail: { - readonly version: string; - readonly release_date: string; - readonly is_latest: boolean; - }; readonly status?: GalleryMcpServerStatus; readonly repository?: { readonly source: string; readonly url: string; readonly id?: string; - readonly subfolder?: string; readonly readme?: string; }; + readonly version_detail: { + readonly version: string; + readonly release_date: string; + readonly is_latest: boolean; + }; readonly created_at: string; readonly updated_at: string; readonly packages?: readonly IRawGalleryMcpServerPackage[]; readonly remotes?: McpServerRemotes; + readonly _meta: { + readonly 'io.modelcontextprotocol.registry': IMcpRegistryInfo; + readonly github?: IGitHubInfo; + }; +} + +interface IRawGalleryMcpServer_2025_07_09 { + readonly $schema?: McpServerSchemaVersion.v2025_07_09; + readonly name: string; + readonly description: string; + readonly status?: GalleryMcpServerStatus; + readonly repository?: { + readonly source: string; + readonly url: string; + readonly id?: string; + readonly readme?: string; + }; + readonly version: string; + readonly website_url?: string; + readonly created_at: string; + readonly updated_at: string; + readonly packages?: readonly IRawGalleryMcpServerPackage[]; + readonly remotes?: McpServerRemotes; + readonly _meta: { + readonly 'io.modelcontextprotocol.registry/official': IMcpRegistryInfo; + readonly 'io.modelcontextprotocol.registry/publisher-provided'?: Record; + }; +} + +type IRawGalleryVersionedMcpServer = + | IRawGalleryMcpServer_2025_01_09 + | IRawGalleryMcpServer_2025_07_09 + ; + +interface IRawGalleryVersionedServersResult { + readonly metadata?: IRawGalleryServerListMetadata; + readonly servers: readonly IRawGalleryVersionedMcpServer[]; +} + +interface IRawGalleryMcpServer { + readonly name: string; + readonly description: string; + readonly status?: GalleryMcpServerStatus; + readonly repository?: { + readonly source: string; + readonly url: string; + readonly id?: string; + readonly readme?: string; + }; + readonly version: string; + readonly websiteUrl?: string; + readonly createdAt: string; + readonly updatedAt: string; + readonly packages?: readonly IRawGalleryMcpServerPackage[]; + readonly remotes?: McpServerRemotes; + readonly registryInfo: IMcpRegistryInfo; + readonly githubInfo?: IGitHubInfo; +} + +interface IRawGalleryServersResult { + readonly metadata?: IRawGalleryServerListMetadata; + readonly servers: readonly IRawGalleryMcpServer[]; } interface IVSCodeGalleryMcpServerDetail { @@ -268,7 +343,7 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService url: gallery.url, }, token); - const result = await asJson(context); + const result = await asJson(context); if (!result) { throw new Error(`Failed to fetch configuration from ${gallery.url}`); } @@ -316,15 +391,12 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService } private toGalleryMcpServer(server: IRawGalleryMcpServer, manifest: IMcpGalleryManifest | null): IGalleryMcpServer { - const registryInfo = server._meta?.['io.modelcontextprotocol.registry/official'] ?? server._meta?.['x-io.modelcontextprotocol.registry']; - const githubInfo = server._meta?.['github'] ?? server._meta?.['x-github']; - let publisher = ''; let displayName = ''; - if (githubInfo?.name) { - displayName = githubInfo.name.split('-').map(s => s.toLowerCase() === 'mcp' ? 'MCP' : s.toLowerCase() === 'github' ? 'GitHub' : uppercaseFirstLetter(s)).join(' '); - publisher = githubInfo.name_with_owner.split('/')[0]; + if (server.githubInfo?.name) { + displayName = server.githubInfo.name.split('-').map(s => s.toLowerCase() === 'mcp' ? 'MCP' : s.toLowerCase() === 'github' ? 'GitHub' : uppercaseFirstLetter(s)).join(' '); + publisher = server.githubInfo.name_with_owner.split('/')[0]; } else { const nameParts = server.name.split('/'); if (nameParts.length > 0) { @@ -336,36 +408,39 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService displayName = nameParts[nameParts.length - 1].split('-').map(s => uppercaseFirstLetter(s)).join(' '); } - const icon: { light: string; dark: string } | undefined = githubInfo?.owner_avatar_url ? { - light: githubInfo.owner_avatar_url, - dark: githubInfo.owner_avatar_url + if (server.githubInfo?.display_name) { + displayName = server.githubInfo.display_name; + } + + const icon: { light: string; dark: string } | undefined = server.githubInfo?.owner_avatar_url ? { + light: server.githubInfo.owner_avatar_url, + dark: server.githubInfo.owner_avatar_url } : undefined; - const serverUrl = manifest ? this.getServerUrl(server.id, manifest) : undefined; + const serverUrl = manifest ? this.getServerUrl(server.registryInfo.id, manifest) : undefined; const webUrl = manifest ? this.getWebUrl(server.name, manifest) : undefined; const publisherUrl = manifest ? this.getPublisherUrl(publisher, manifest) : undefined; return { - id: server.id, + id: server.registryInfo.id, name: server.name, displayName, url: serverUrl, webUrl, description: server.description, status: server.status ?? GalleryMcpServerStatus.Active, - version: server.version_detail.version, - isLatest: server.version_detail.is_latest, - releaseDate: Date.parse(server.version_detail.release_date), - publishDate: registryInfo ? Date.parse(registryInfo.published_at) : undefined, - lastUpdated: githubInfo?.pushed_at ? Date.parse(githubInfo.pushed_at) : registryInfo ? Date.parse(registryInfo.updated_at) : undefined, + version: server.version, + isLatest: server.registryInfo.is_latest, + publishDate: server.registryInfo.published_at ? Date.parse(server.registryInfo.published_at) : undefined, + lastUpdated: server.githubInfo?.pushed_at ? Date.parse(server.githubInfo.pushed_at) : server.registryInfo ? Date.parse(server.registryInfo.updated_at) : undefined, repositoryUrl: server.repository?.url, readme: server.repository?.readme, icon, publisher, publisherUrl, - license: githubInfo?.license, - starsCount: githubInfo?.stargazer_count, - topics: githubInfo?.topics, + license: server.githubInfo?.license, + starsCount: server.githubInfo?.stargazer_count, + topics: server.githubInfo?.topics, configuration: this.toGalleryMcpServerConfiguration(server.packages, server.remotes) }; } @@ -406,7 +481,7 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService } private async queryRawGalleryMcpServers(query: Query, mcpGalleryManifest: IMcpGalleryManifest, token: CancellationToken): Promise { - const mcpGalleryUrl = this.getMcpGalleryUrl(mcpGalleryManifest); + const mcpGalleryUrl = query.searchText ? this.getSearchUrl(mcpGalleryManifest) : this.getMcpGalleryUrl(mcpGalleryManifest); if (!mcpGalleryUrl) { return { servers: [] }; } @@ -428,7 +503,7 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService } if (query.searchText) { const text = encodeURIComponent(query.searchText); - url += `&search=${text}`; + url += `&q=${text}`; } const context = await this.requestService.request({ @@ -436,20 +511,16 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService url, }, token); - const result = await asJson(context); + const result = await asJson(context); if (!result) { return { servers: [] }; } - if (isIRawGalleryServersOldResult(result)) { - return { - servers: result.servers.map(server => this.toIRawGalleryMcpServer(server)), - metadata: result.metadata - }; - } - - return result; + return { + servers: result.servers.map(server => this.toIRawGalleryMcpServer(server)), + metadata: result.metadata + }; } async getMcpServer(mcpServerUrl: string, mcpGalleryManifest?: IMcpGalleryManifest | null): Promise { @@ -462,7 +533,7 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService return undefined; } - const server = await asJson(context); + const server = await asJson(context); if (!server) { return undefined; } @@ -497,7 +568,7 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService return undefined; } - const server = await asJson(context); + const server = await asJson(context); if (!server) { return undefined; } @@ -505,18 +576,63 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService return this.toGalleryMcpServer(this.toIRawGalleryMcpServer(server), mcpGalleryManifest); } - private toIRawGalleryMcpServer(from: IRawGalleryOldMcpServer | IRawGalleryMcpServer): IRawGalleryMcpServer { + private toIRawGalleryMcpServer(from: IRawGalleryOldMcpServer | IRawGalleryVersionedMcpServer): IRawGalleryMcpServer { if (isIRawGalleryOldMcpServer(from)) { return { - ...from.server, - _meta: { - 'io.modelcontextprotocol.registry/official': from['io.modelcontextprotocol.registry/official'] ?? from['x-io.modelcontextprotocol.registry'], - 'github': from['x-github'], - 'io.modelcontextprotocol.registry/publisher-provided': from['io.modelcontextprotocol.registry/publisher-provided'] ?? from['x-publisher'] - } + name: from.server.name, + description: from.server.description, + status: from.server.status, + repository: from.server.repository ? { + url: from.server.repository.url, + source: from.server.repository.source, + id: from.server.repository.id, + readme: from.server.repository.readme + } : undefined, + version: from.server.version_detail.version, + createdAt: from.server.created_at, + updatedAt: from.server.updated_at, + packages: from.server.packages, + remotes: from.server.remotes, + registryInfo: from['x-io.modelcontextprotocol.registry'], + githubInfo: from['x-github'], }; } - return from; + + if (from.$schema === McpServerSchemaVersion.v2025_01_09) { + const server = from; + return { + name: server.name, + description: server.description, + status: server.status, + repository: server.repository, + version: server.version_detail.version, + createdAt: server.created_at, + updatedAt: server.updated_at, + packages: server.packages, + remotes: server.remotes, + registryInfo: server._meta['io.modelcontextprotocol.registry'], + githubInfo: server._meta.github + }; + } + + if (from.$schema === McpServerSchemaVersion.v2025_07_09) { + const server = from; + return { + name: server.name, + description: server.description, + status: server.status, + repository: server.repository, + version: server.version, + createdAt: server.created_at, + updatedAt: server.updated_at, + packages: server.packages, + remotes: server.remotes, + registryInfo: server._meta['io.modelcontextprotocol.registry/official'], + githubInfo: server._meta['io.modelcontextprotocol.registry/publisher-provided']?.github + }; + } + + throw new Error('Unsupported MCP server schema version'); } private async fetchMcpServersFromVSCodeGallery(): Promise { @@ -574,6 +690,10 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService return format2(namedResourceUriTemplate, { name }); } + private getSearchUrl(mcpGalleryManifest: IMcpGalleryManifest): string | undefined { + return getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServersSearchService); + } + private getWebUrl(name: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined { const resourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerWebUri); if (!resourceUriTemplate) { diff --git a/src/vs/platform/mcp/common/mcpManagement.ts b/src/vs/platform/mcp/common/mcpManagement.ts index c142a80349d..2e236c4b6a2 100644 --- a/src/vs/platform/mcp/common/mcpManagement.ts +++ b/src/vs/platform/mcp/common/mcpManagement.ts @@ -143,7 +143,6 @@ export interface IGalleryMcpServer { }; readonly lastUpdated?: number; readonly publishDate?: number; - readonly releaseDate?: number; readonly repositoryUrl?: string; readonly configuration?: IGalleryMcpServerConfiguration; readonly readmeUrl?: string; diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts index 0f01fc06138..923db9458b4 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -7,7 +7,7 @@ import * as dom from '../../../base/browser/dom.js'; import { RunOnceScheduler } from '../../../base/common/async.js'; import { VSBuffer } from '../../../base/common/buffer.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; import { ISocket, SocketCloseEvent, SocketCloseEventType, SocketDiagnostics, SocketDiagnosticsEventType } from '../../../base/parts/ipc/common/ipc.net.js'; import { ISocketFactory } from '../common/remoteSocketFactoryService.js'; import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode, RemoteConnectionType, WebSocketRemoteConnection } from '../common/remoteAuthorityResolver.js'; @@ -282,11 +282,12 @@ export class BrowserSocketFactory implements ISocketFactory((resolve, reject) => { const webSocketSchema = (/^https:/.test(mainWindow.location.href) ? 'wss' : 'ws'); const socket = this._webSocketFactory.create(`${webSocketSchema}://${(/:/.test(host) && !/\[/.test(host)) ? `[${host}]` : host}:${port}${path}?${query}&skipWebSocketFrames=false`, debugLabel); - const errorListener = socket.onError(reject); - socket.onOpen(() => { - errorListener.dispose(); + const disposables = new DisposableStore(); + disposables.add(socket.onError(reject)); + disposables.add(socket.onOpen(() => { + disposables.dispose(); resolve(new BrowserSocket(socket, debugLabel)); - }); + })); }); } } diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index b942f71a8f2..5e320a7843d 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -11,7 +11,6 @@ import { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability, ITer import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs } from './terminalProcess.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { ISerializableEnvironmentVariableCollections } from './environmentVariable.js'; -import { RawContextKey } from '../../contextkey/common/contextkey.js'; import { IWorkspaceFolder } from '../../workspace/common/workspace.js'; import { Registry } from '../../registry/common/platform.js'; import type * as performance from '../../../base/common/performance.js'; @@ -19,8 +18,6 @@ import { ILogService } from '../../log/common/log.js'; import type { IAction } from '../../../base/common/actions.js'; import type { IDisposable } from '../../../base/common/lifecycle.js'; -export const terminalTabFocusModeContextKey = new RawContextKey('terminalTabFocusMode', false, true); - export const enum TerminalSettingPrefix { AutomationProfile = 'terminal.integrated.automationProfile.', DefaultProfile = 'terminal.integrated.defaultProfile.', diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 57d8b534e68..52317939863 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import electron, { Display, Rectangle } from 'electron'; +import { release } from 'os'; import { Color } from '../../../base/common/color.js'; import { Event } from '../../../base/common/event.js'; import { join } from '../../../base/common/path.js'; -import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js'; +import { IProcessEnvironment, isLinux, isMacintosh, isTahoe, isWindows } from '../../../base/common/platform.js'; import { URI } from '../../../base/common/uri.js'; import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryWindow.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; @@ -186,6 +187,14 @@ export function defaultBrowserWindowOptions(accessor: ServicesAccessor, windowSt if (windowSettings?.clickThroughInactive === false) { options.acceptFirstMouse = false; } + + // Mac OS 26.?.? has a `WindowServer` bug that causes (some?) windows with shadows + // to cause 80%+ GPU load. + // See: https://github.com/electron/electron/issues/48311 + // TODO: once the bug is fixed in the OS, lock this into a specific version. + if (isTahoe(release())) { + options.hasShadow = false; + } } if (overrides?.disableFullscreen) { diff --git a/src/vs/server/node/remoteAgentEnvironmentImpl.ts b/src/vs/server/node/remoteAgentEnvironmentImpl.ts index 970a974be84..0884af22d54 100644 --- a/src/vs/server/node/remoteAgentEnvironmentImpl.ts +++ b/src/vs/server/node/remoteAgentEnvironmentImpl.ts @@ -7,7 +7,7 @@ import { Event } from '../../base/common/event.js'; import * as platform from '../../base/common/platform.js'; import * as performance from '../../base/common/performance.js'; import { URI } from '../../base/common/uri.js'; -import { createURITransformer } from '../../workbench/api/node/uriTransformer.js'; +import { createURITransformer } from '../../base/common/uriTransformer.js'; import { IRemoteAgentEnvironmentDTO, IGetEnvironmentDataArguments, IGetExtensionHostExitInfoArguments } from '../../workbench/services/remote/common/remoteAgentEnvironmentChannel.js'; import { IServerEnvironmentService } from './serverEnvironmentService.js'; import { IServerChannel } from '../../base/parts/ipc/common/ipc.js'; diff --git a/src/vs/server/node/remoteFileSystemProviderServer.ts b/src/vs/server/node/remoteFileSystemProviderServer.ts index 3ab5969e576..f97d2cc97d1 100644 --- a/src/vs/server/node/remoteFileSystemProviderServer.ts +++ b/src/vs/server/node/remoteFileSystemProviderServer.ts @@ -8,7 +8,7 @@ import { URI, UriComponents } from '../../base/common/uri.js'; import { IURITransformer } from '../../base/common/uriIpc.js'; import { IFileChange } from '../../platform/files/common/files.js'; import { ILogService } from '../../platform/log/common/log.js'; -import { createURITransformer } from '../../workbench/api/node/uriTransformer.js'; +import { createURITransformer } from '../../base/common/uriTransformer.js'; import { RemoteAgentConnectionContext } from '../../platform/remote/common/remoteAgentEnvironment.js'; import { DiskFileSystemProvider } from '../../platform/files/node/diskFileSystemProvider.js'; import { posix, delimiter } from '../../base/common/path.js'; diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index 1cb1e36c5f0..a97e55c8480 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -17,7 +17,7 @@ import { RemoteAgentConnectionContext } from '../../platform/remote/common/remot import { IPtyHostService, IShellLaunchConfig, ITerminalProfile } from '../../platform/terminal/common/terminal.js'; import { IGetTerminalLayoutInfoArgs, ISetTerminalLayoutInfoArgs } from '../../platform/terminal/common/terminalProcess.js'; import { IWorkspaceFolder } from '../../platform/workspace/common/workspace.js'; -import { createURITransformer } from '../../workbench/api/node/uriTransformer.js'; +import { createURITransformer } from '../../base/common/uriTransformer.js'; import { CLIServerBase, ICommandsExecuter } from '../../workbench/api/node/extHostCLIServer.js'; import { IEnvironmentVariableCollection } from '../../platform/terminal/common/environmentVariable.js'; import { MergedEnvironmentVariableCollection } from '../../platform/terminal/common/environmentVariableCollection.js'; diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index b80d2006fea..1e6758f1c88 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -55,7 +55,7 @@ import { RemoteAgentFileSystemProviderChannel } from './remoteFileSystemProvider import { ServerTelemetryChannel } from '../../platform/telemetry/common/remoteTelemetryChannel.js'; import { IServerTelemetryService, ServerNullTelemetryService, ServerTelemetryService } from '../../platform/telemetry/common/serverTelemetryService.js'; import { RemoteTerminalChannel } from './remoteTerminalChannel.js'; -import { createURITransformer } from '../../workbench/api/node/uriTransformer.js'; +import { createURITransformer } from '../../base/common/uriTransformer.js'; import { ServerConnectionToken } from './serverConnectionToken.js'; import { ServerEnvironmentService, ServerParsedArgs } from './serverEnvironmentService.js'; import { REMOTE_TERMINAL_CHANNEL_NAME } from '../../workbench/contrib/terminal/common/remote/remoteTerminalChannel.js'; diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index ff002feb111..8e754163168 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -11,10 +11,12 @@ import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuth import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js'; import Severity from '../../../base/common/severity.js'; import { INotificationService } from '../../../platform/notification/common/notification.js'; +import { ActivationKind, IExtensionService } from '../../services/extensions/common/extensions.js'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { IAuthenticationAccessService } from '../../services/authentication/browser/authenticationAccessService.js'; import { IAuthenticationUsageService } from '../../services/authentication/browser/authenticationUsageService.js'; +import { getAuthenticationProviderActivationEvent } from '../../services/authentication/browser/authenticationService.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { IOpenerService } from '../../../platform/opener/common/opener.js'; import { CancellationError } from '../../../base/common/errors.js'; @@ -116,6 +118,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu @IAuthenticationUsageService private readonly authenticationUsageService: IAuthenticationUsageService, @IDialogService private readonly dialogService: IDialogService, @INotificationService private readonly notificationService: INotificationService, + @IExtensionService private readonly extensionService: IExtensionService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IOpenerService private readonly openerService: IOpenerService, @ILogService private readonly logService: ILogService, @@ -200,6 +203,12 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } } + async $ensureProvider(id: string): Promise { + if (!this.authenticationService.isAuthenticationProviderRegistered(id)) { + return await this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id), ActivationKind.Immediate); + } + } + async $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): Promise { const obj = this._registrations.get(providerId); if (obj instanceof Emitter) { diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index e82c4365e5b..c7ee8e4650e 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -169,7 +169,8 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA invoke: async (request, progress, history, token) => { this._pendingProgress.set(request.requestId, progress); try { - return await this._proxy.$invokeAgent(handle, request, { history }, token) ?? {}; + const chatSessionContext = this._chatService.getChatSessionFromInternalId(request.sessionId); + return await this._proxy.$invokeAgent(handle, request, { history, chatSessionContext }, token) ?? {}; } finally { this._pendingProgress.delete(request.requestId); } @@ -220,7 +221,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._agents.set(handle, { id: id, extensionId: extension, - dispose: disposable.dispose, + dispose: () => disposable.dispose(), hasFollowups: metadata.hasFollowups }); } diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 1b91525c868..b3f0e49a1ad 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -20,6 +20,7 @@ import { IChatContentInlineReference, IChatProgress } from '../../contrib/chat/c import { ChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; import { ChatSessionUri } from '../../contrib/chat/common/chatUri.js'; import { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js'; +import { IEditorGroup, IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../services/editor/common/editorService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; @@ -317,6 +318,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat @IChatSessionsService private readonly _chatSessionsService: IChatSessionsService, @IDialogService private readonly _dialogService: IDialogService, @IEditorService private readonly _editorService: IEditorService, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @ILogService private readonly _logService: ILogService, @IViewsService private readonly _viewsService: IViewsService, ) { @@ -349,6 +351,50 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat this._itemProvidersRegistrations.get(handle)?.onDidChangeItems.fire(); } + $onDidCommitChatSessionItem(handle: number, original: string, modified: string): void { + this._logService.trace(`$onDidCommitChatSessionItem: handle(${handle}), original(${original}), modified(${modified})`); + const chatSessionType = this._itemProvidersRegistrations.get(handle)?.provider.chatSessionType; + if (!chatSessionType) { + this._logService.error(`No chat session type found for provider handle ${handle}`); + return; + } + const originalResource = ChatSessionUri.forSession(chatSessionType, original); + const modifiedResource = ChatSessionUri.forSession(chatSessionType, modified); + const originalEditor = this._editorService.editors.find(editor => editor.resource?.toString() === originalResource.toString()); + + // Find the group containing the original editor + let originalGroup: IEditorGroup | undefined; + for (const group of this.editorGroupService.groups) { + if (group.editors.some(editor => editor.resource?.toString() === originalResource.toString())) { + originalGroup = group; + break; + } + } + if (!originalGroup) { + originalGroup = this.editorGroupService.activeGroup; + } + + if (originalEditor) { + // Prefetch the chat session content to make the subsequent editor swap quick + this._chatSessionsService.provideChatSessionContent( + chatSessionType, + modified, + CancellationToken.None, + ).then(() => { + this._editorService.replaceEditors([{ + editor: originalEditor, + replacement: { + resource: modifiedResource, + options: {} + }, + }], originalGroup); + }); + } else { + this._logService.warn(`Original chat session editor not found for resource ${originalResource.toString()}`); + this._editorService.openEditor({ resource: modifiedResource }, originalGroup); + } + } + private async _provideChatSessionItems(handle: number, token: CancellationToken): Promise { try { // Get all results as an array from the RPC call @@ -365,7 +411,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat return []; } - private async _provideNewChatSessionItem(handle: number, options: { request: IChatAgentRequest; prompt?: string; history?: any[]; metadata?: any }, token: CancellationToken): Promise { + private async _provideNewChatSessionItem(handle: number, options: { request: IChatAgentRequest; metadata?: any }, token: CancellationToken): Promise { try { const chatSessionItem = await this._proxy.$provideNewChatSessionItem(handle, options, token); if (!chatSessionItem) { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 81a3bdb2b6d..da6f1fe6769 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -631,6 +631,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[], supportsHandleEvents: boolean, extensionId: string, extensionVersion: string, groupId: string | undefined, yieldsToExtensionIds: string[], displayName: string | undefined, debounceDelayMs: number | undefined, excludesExtensionIds: string[], eventHandle: number | undefined): void { + const providerId = new languages.ProviderId(extensionId, extensionVersion, groupId); const provider: languages.InlineCompletionsProvider = { provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { const result = await this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token); @@ -641,12 +642,13 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread const aiEditTelemetryService = accessor.getIfExists(IAiEditTelemetryService); item.suggestionId = aiEditTelemetryService?.createSuggestionId({ applyCodeBlockSuggestionId: undefined, - editDeltaInfo: new EditDeltaInfo(1, 1, -1, -1), // TODO@hediet, fix this approximation. feature: 'inlineSuggestion', + source: providerId, languageId: completions.languageId, + editDeltaInfo: new EditDeltaInfo(1, 1, -1, -1), // TODO@hediet, fix this approximation. modeId: undefined, modelId: undefined, - presentation: 'inlineSuggestion', + presentation: item.isInlineEdit ? 'nextEditSuggestion' : 'inlineCompletion', }); }); @@ -680,17 +682,18 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread const aiEditTelemetryService = accessor.getIfExists(IAiEditTelemetryService); aiEditTelemetryService?.handleCodeAccepted({ suggestionId: item.suggestionId, + feature: 'inlineSuggestion', + source: providerId, + languageId: completions.languageId, editDeltaInfo: EditDeltaInfo.tryCreate( lifetimeSummary.lineCountModified, lifetimeSummary.lineCountOriginal, lifetimeSummary.characterCountModified, lifetimeSummary.characterCountOriginal, ), - feature: 'inlineSuggestion', - languageId: completions.languageId, modeId: undefined, modelId: undefined, - presentation: 'inlineSuggestion', + presentation: item.isInlineEdit ? 'nextEditSuggestion' : 'inlineCompletion', acceptanceMethod: 'accept', applyCodeBlockSuggestionId: undefined, }); @@ -749,7 +752,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } }, groupId: groupId ?? extensionId, - providerId: new languages.ProviderId(extensionId, extensionVersion, groupId), + providerId, yieldsToGroupIds: yieldsToExtensionIds, excludesGroupIds: excludesExtensionIds, debounceDelayMs, diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 522e6490312..9f71ac0cfb4 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -56,8 +56,11 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre token ?? CancellationToken.None, ); - // Don't return extra metadata to EH - const out: Dto = { content: result.content }; + // Only return content and metadata to EH + const out: Dto = { + content: result.content, + toolMetadata: result.toolMetadata + }; return toolResultHasBuffers(result) ? new SerializableObjectWithBuffers(out) : out; } diff --git a/src/vs/workbench/api/browser/mainThreadMcp.ts b/src/vs/workbench/api/browser/mainThreadMcp.ts index 53bba4a2581..2dcf4444bb0 100644 --- a/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -3,26 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { mapFindFirst } from '../../../base/common/arraysFind.js'; import { disposableTimeout } from '../../../base/common/async.js'; import { CancellationError } from '../../../base/common/errors.js'; import { Emitter } from '../../../base/common/event.js'; -import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore, MutableDisposable } from '../../../base/common/lifecycle.js'; import { IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata } from '../../../base/common/oauth.js'; import { ISettableObservable, observableValue } from '../../../base/common/observable.js'; import Severity from '../../../base/common/severity.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import * as nls from '../../../nls.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../platform/contextkey/common/contextkey.js'; import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { LogLevel } from '../../../platform/log/common/log.js'; import { IMcpMessageTransport, IMcpRegistry } from '../../contrib/mcp/common/mcpRegistryTypes.js'; -import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js'; +import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js'; import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js'; import { IAuthenticationMcpAccessService } from '../../services/authentication/browser/authenticationMcpAccessService.js'; import { IAuthenticationMcpService } from '../../services/authentication/browser/authenticationMcpService.js'; import { IAuthenticationMcpUsageService } from '../../services/authentication/browser/authenticationMcpUsageService.js'; import { AuthenticationSession, AuthenticationSessionAccount, IAuthenticationService } from '../../services/authentication/common/authentication.js'; import { ExtensionHostKind, extensionHostKindToString } from '../../services/extensions/common/extensionHostKind.js'; +import { IExtensionService } from '../../services/extensions/common/extensions.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { Proxied } from '../../services/extensions/common/proxyIdentifier.js'; import { ExtHostContext, ExtHostMcpShape, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js'; @@ -36,7 +39,6 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { private readonly _serverDefinitions = new Map(); private readonly _proxy: Proxied; private readonly _collectionDefinitions = this._register(new DisposableMap; dispose(): void; }>()); @@ -49,6 +51,8 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { @IAuthenticationMcpService private readonly authenticationMcpServersService: IAuthenticationMcpService, @IAuthenticationMcpAccessService private readonly authenticationMCPServerAccessService: IAuthenticationMcpAccessService, @IAuthenticationMcpUsageService private readonly authenticationMCPServerUsageService: IAuthenticationMcpUsageService, + @IExtensionService private readonly _extensionService: IExtensionService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, ) { super(); const proxy = this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostMcp); @@ -67,6 +71,10 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { } return true; }, + async substituteVariables(serverDefinition, launch) { + const ser = await proxy.$substituteVariables(serverDefinition.variableReplacement?.folder?.uri, McpServerLaunch.toSerialized(launch)); + return McpServerLaunch.fromSerialized(ser); + }, start: (_collection, serverDefiniton, resolveLaunch, options) => { const id = ++this._serverIdCounter; const launch = new ExtHostMcpServerLaunch( @@ -76,7 +84,11 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { ); this._servers.set(id, launch); this._serverDefinitions.set(id, serverDefiniton); - proxy.$startMcp(id, resolveLaunch, options?.errorOnUserInteraction); + proxy.$startMcp(id, { + launch: resolveLaunch, + defaultCwd: serverDefiniton.variableReplacement?.folder?.uri, + errorOnUserInteraction: options?.errorOnUserInteraction, + }); return launch; }, @@ -90,22 +102,47 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { existing.servers.set(servers, undefined); } else { const serverDefinitions = observableValue('mcpServers', servers); - const handle = this._mcpRegistry.registerCollection({ - ...collection, - source: new ExtensionIdentifier(collection.extensionId), - resolveServerLanch: collection.canResolveLaunch ? (async def => { - const r = await this._proxy.$resolveMcpLaunch(collection.id, def.label); - return r ? McpServerLaunch.fromSerialized(r) : undefined; - }) : undefined, - trustBehavior: collection.isTrustedByDefault ? McpServerTrust.Kind.Trusted : McpServerTrust.Kind.TrustedOnNonce, - remoteAuthority: this._extHostContext.remoteAuthority, - serverDefinitions, - }); + const extensionId = new ExtensionIdentifier(collection.extensionId); + const store = new DisposableStore(); + const handle = new MutableDisposable(); + const register = () => { + handle.value ??= this._mcpRegistry.registerCollection({ + ...collection, + source: extensionId, + resolveServerLanch: collection.canResolveLaunch ? (async def => { + const r = await this._proxy.$resolveMcpLaunch(collection.id, def.label); + return r ? McpServerLaunch.fromSerialized(r) : undefined; + }) : undefined, + trustBehavior: collection.isTrustedByDefault ? McpServerTrust.Kind.Trusted : McpServerTrust.Kind.TrustedOnNonce, + remoteAuthority: this._extHostContext.remoteAuthority, + serverDefinitions, + }); + }; + + const whenClauseStr = mapFindFirst(this._extensionService.extensions, e => + ExtensionIdentifier.equals(extensionId, e.identifier) + ? e.contributes?.mcpServerDefinitionProviders?.find(p => extensionPrefixedIdentifier(extensionId, p.id) === collection.id)?.when + : undefined); + const whenClause = whenClauseStr && ContextKeyExpr.deserialize(whenClauseStr); + + if (!whenClause) { + register(); + } else { + const evaluate = () => { + if (this._contextKeyService.contextMatchesRules(whenClause)) { + register(); + } else { + handle.clear(); + } + }; + + store.add(this._contextKeyService.onDidChangeContext(evaluate)); + evaluate(); + } this._collectionDefinitions.set(collection.id, { - fromExtHost: collection, servers: serverDefinitions, - dispose: () => handle.dispose(), + dispose: () => store.dispose(), }); } } diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 1736a8e5d24..0674b8ff0f6 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -259,12 +259,12 @@ const viewsExtensionPoint: IExtensionPoint = ExtensionsR extensionPoint: 'views', deps: [viewsContainersExtensionPoint], jsonSchema: viewsContribution, - activationEventsGenerator: (viewExtensionPointTypeArray, result) => { + activationEventsGenerator: function* (viewExtensionPointTypeArray) { for (const viewExtensionPointType of viewExtensionPointTypeArray) { for (const viewDescriptors of Object.values(viewExtensionPointType)) { for (const viewDescriptor of viewDescriptors) { if (viewDescriptor.id) { - result.push(`onView:${viewDescriptor.id}`); + yield `onView:${viewDescriptor.id}`; } } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 894bd1b233a..690e0ef0479 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -224,6 +224,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostChatOutputRenderer = rpcProtocol.set(ExtHostContext.ExtHostChatOutputRenderer, new ExtHostChatOutputRenderer(rpcProtocol, extHostWebviews)); rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService)); const extHostLanguageModelTools = rpcProtocol.set(ExtHostContext.ExtHostLanguageModelTools, new ExtHostLanguageModelTools(rpcProtocol, extHostLanguageModels)); + const extHostChatSessions = rpcProtocol.set(ExtHostContext.ExtHostChatSessions, new ExtHostChatSessions(extHostCommands, extHostLanguageModels, rpcProtocol, extHostLogService)); const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostLanguageModels, extHostDiagnostics, extHostLanguageModelTools)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); @@ -231,7 +232,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostStatusBar = rpcProtocol.set(ExtHostContext.ExtHostStatusBar, new ExtHostStatusBar(rpcProtocol, extHostCommands.converter)); const extHostSpeech = rpcProtocol.set(ExtHostContext.ExtHostSpeech, new ExtHostSpeech(rpcProtocol)); const extHostEmbeddings = rpcProtocol.set(ExtHostContext.ExtHostEmbeddings, new ExtHostEmbeddings(rpcProtocol)); - const extHostChatSessions = rpcProtocol.set(ExtHostContext.ExtHostChatSessions, new ExtHostChatSessions(extHostCommands, extHostLanguageModels, rpcProtocol, extHostLogService)); rpcProtocol.set(ExtHostContext.ExtHostMcp, accessor.get(IExtHostMpcService)); @@ -302,9 +302,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const authentication: typeof vscode.authentication = { getSession(providerId: string, scopesOrChallenge: readonly string[] | vscode.AuthenticationWwwAuthenticateRequest, options?: vscode.AuthenticationGetSessionOptions) { - if (!Array.isArray(scopesOrChallenge)) { - checkProposedApiEnabled(extension, 'authenticationChallenges'); - } if ( (typeof options?.forceNewSession === 'object' && options.forceNewSession.learnMore) || (typeof options?.createIfNone === 'object' && options.createIfNone.learnMore) @@ -1530,9 +1527,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatSessionsProvider'); return extHostChatSessions.registerChatSessionItemProvider(extension, chatSessionType, provider); }, - registerChatSessionContentProvider(chatSessionType: string, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities) { + registerChatSessionContentProvider(chatSessionType: string, provider: vscode.ChatSessionContentProvider, chatParticipant: vscode.ChatParticipant, capabilities?: vscode.ChatSessionCapabilities) { checkProposedApiEnabled(extension, 'chatSessionsProvider'); - return extHostChatSessions.registerChatSessionContentProvider(extension, chatSessionType, provider, capabilities); + return extHostChatSessions.registerChatSessionContentProvider(extension, chatSessionType, chatParticipant, provider, capabilities); }, registerChatOutputRenderer: (viewType: string, renderer: vscode.ChatOutputRenderer) => { checkProposedApiEnabled(extension, 'chatOutputRenderer'); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 12e3fcdcdf4..2359a09ee3d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -189,6 +189,7 @@ export interface AuthenticationGetSessionOptions { export interface MainThreadAuthenticationShape extends IDisposable { $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean, supportedAuthorizationServers?: UriComponents[], supportsChallenges?: boolean): Promise; $unregisterAuthenticationProvider(id: string): Promise; + $ensureProvider(id: string): Promise; $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): Promise; $getSession(providerId: string, scopeListOrRequest: ReadonlyArray | IAuthenticationWwwAuthenticateRequest, extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise; $getAccounts(providerId: string): Promise>; @@ -1368,7 +1369,7 @@ export type IChatAgentHistoryEntryDto = { }; export interface ExtHostChatAgentsShape2 { - $invokeAgent(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; + $invokeAgent(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: { chatSessionType: string; chatSessionId: string; isUntitled: boolean } }, token: CancellationToken): Promise; $provideFollowups(request: Dto, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $acceptFeedback(handle: number, result: IChatAgentResult, voteAction: IChatVoteAction): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; @@ -3017,9 +3018,16 @@ export interface ExtHostTestingShape { $disposeTestFollowups(id: number[]): void; } +export interface IStartMcpOptions { + launch: McpServerLaunch.Serialized; + defaultCwd?: UriComponents; + errorOnUserInteraction?: boolean; +} + export interface ExtHostMcpShape { + $substituteVariables(workspaceFolder: UriComponents | undefined, value: McpServerLaunch.Serialized): Promise; $resolveMcpLaunch(collectionId: string, label: string): Promise; - $startMcp(id: number, launch: McpServerLaunch.Serialized, errorOnUserInteraction?: boolean): void; + $startMcp(id: number, opts: IStartMcpOptions): void; $stopMcp(id: number): void; $sendMessage(id: number, message: string): void; $waitForInitialCollectionProviders(): Promise; @@ -3143,7 +3151,7 @@ export interface MainThreadChatSessionsShape extends IDisposable { $registerChatSessionItemProvider(handle: number, chatSessionType: string): void; $unregisterChatSessionItemProvider(handle: number): void; $onDidChangeChatSessionItems(handle: number): void; - + $onDidCommitChatSessionItem(handle: number, original: string, modified: string): void; $registerChatSessionContentProvider(handle: number, chatSessionType: string): void; $unregisterChatSessionContentProvider(handle: number): void; @@ -3156,7 +3164,7 @@ export interface MainThreadChatSessionsShape extends IDisposable { export interface ExtHostChatSessionsShape { $provideChatSessionItems(providerHandle: number, token: CancellationToken): Promise[]>; - $provideNewChatSessionItem(providerHandle: number, options: { request: IChatAgentRequest; prompt?: string; history?: any[]; metadata?: any }, token: CancellationToken): Promise>; + $provideNewChatSessionItem(providerHandle: number, options: { request: IChatAgentRequest; metadata?: any }, token: CancellationToken): Promise>; $provideChatSessionContent(providerHandle: number, sessionId: string, token: CancellationToken): Promise; $interruptChatSessionActiveResponse(providerHandle: number, sessionId: string, requestId: string): Promise; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index dd829afab53..27a36da458a 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -88,18 +88,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { const keys: (keyof vscode.AuthenticationGetSessionOptions)[] = Object.keys(options) as (keyof vscode.AuthenticationGetSessionOptions)[]; const optionsStr = keys.sort().map(key => `${key}:${!!options[key]}`).join(', '); - // old shape, remove next milestone - if ( - 'scopes' in scopesOrRequest - && typeof scopesOrRequest.scopes === 'string' - && !scopesOrRequest.fallbackScopes - ) { - scopesOrRequest = { - wwwAuthenticate: scopesOrRequest.wwwAuthenticate, - fallbackScopes: scopesOrRequest.scopes - }; - } - let singlerKey: string; if (isAuthenticationWwwAuthenticateRequest(scopesOrRequest)) { const challenge = scopesOrRequest as vscode.AuthenticationWwwAuthenticateRequest; @@ -112,12 +100,14 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { } return await this._getSessionTaskSingler.getOrCreate(singlerKey, async () => { + await this._proxy.$ensureProvider(providerId); const extensionName = requestingExtension.displayName || requestingExtension.name; return this._proxy.$getSession(providerId, scopesOrRequest, extensionId, extensionName, options); }); } async getAccounts(providerId: string) { + await this._proxy.$ensureProvider(providerId); return await this._proxy.$getAccounts(providerId); } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index a8e1a259907..452c3273a73 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -541,7 +541,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS this._onDidChangeChatRequestTools.fire(request.extRequest); } - async $invokeAgent(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { + async $invokeAgent(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: { chatSessionType: string; chatSessionId: string; isUntitled: boolean } }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { throw new Error(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`); @@ -575,9 +575,23 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS inFlightRequest = { requestId: requestDto.requestId, extRequest, extension: agent.extension }; this._inFlightRequests.add(inFlightRequest); + + // If this request originates from a contributed chat session editor, attempt to resolve the ChatSession API object + let chatSessionContext: vscode.ChatSessionContext | undefined; + if (context.chatSessionContext) { + chatSessionContext = { + chatSessionItem: { + id: context.chatSessionContext.chatSessionId, + label: context.chatSessionContext.isUntitled ? 'Untitled Session' : 'Session', + }, + isUntitled: context.chatSessionContext.isUntitled, + }; + } + + const chatContext: vscode.ChatContext = { history, chatSessionContext }; const task = agent.invoke( extRequest, - { history }, + chatContext, stream.apiObject, token ); @@ -599,7 +613,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS responseIsIncomplete: true }; } - if (errorDetails?.responseIsRedacted || errorDetails?.isQuotaExceeded || errorDetails?.confirmationButtons || errorDetails?.code) { + if (errorDetails?.responseIsRedacted || errorDetails?.isQuotaExceeded || errorDetails?.isRateLimited || errorDetails?.confirmationButtons || errorDetails?.code) { checkProposedApiEnabled(agent.extension, 'chatParticipantPrivate'); } @@ -613,7 +627,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS } const isQuotaExceeded = e instanceof Error && e.name === 'ChatQuotaExceeded'; - return { errorDetails: { message: toErrorMessage(e), responseIsIncomplete: true, isQuotaExceeded } }; + const isRateLimited = e instanceof Error && e.name === 'ChatRateLimited'; + return { errorDetails: { message: toErrorMessage(e), responseIsIncomplete: true, isQuotaExceeded, isRateLimited } }; } finally { if (inFlightRequest) { diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index c7dbfe89330..670fec02512 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -103,6 +103,12 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio this._proxy.$onDidChangeChatSessionItems(handle); })); } + if (provider.onDidCommitChatSessionItem) { + disposables.add(provider.onDidCommitChatSessionItem((e) => { + const { original, modified } = e; + this._proxy.$onDidCommitChatSessionItem(handle, original.id, modified.id); + })); + } return { dispose: () => { this._chatSessionItemProviders.delete(handle); @@ -112,7 +118,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }; } - registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionType: string, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable { + registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionType: string, chatParticipant: vscode.ChatParticipant, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable { const handle = this._nextChatSessionContentProviderHandle++; const disposables = new DisposableStore(); @@ -164,7 +170,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }; } - async $provideNewChatSessionItem(handle: number, options: { request: IChatAgentRequest; prompt?: string; history: any[]; metadata?: any }, token: CancellationToken): Promise { + async $provideNewChatSessionItem(handle: number, options: { request: IChatAgentRequest; metadata?: any }, token: CancellationToken): Promise { const entry = this._chatSessionItemProviders.get(handle); if (!entry || !entry.provider.provideNewChatSessionItem) { throw new Error(`No provider registered for handle ${handle} or provider does not support creating sessions`); @@ -183,8 +189,6 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio const vscodeOptions = { request: vscodeRequest, - prompt: options.prompt, - history: options.history, metadata: options.metadata }; diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index ced3a2ea275..77fd72afb3e 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -14,7 +14,7 @@ import { URI, UriComponents } from '../../../base/common/uri.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { ISignService } from '../../../platform/sign/common/sign.js'; -import { IWorkspaceFolder } from '../../../platform/workspace/common/workspace.js'; +import { IWorkspaceFolderData } from '../../../platform/workspace/common/workspace.js'; import { AbstractDebugAdapter } from '../../contrib/debug/common/abstractDebugAdapter.js'; import { DebugVisualizationType, IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterImpl, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebuggerContribution, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { convertToDAPaths, convertToVSCPaths, isDebuggerMainContribution } from '../../contrib/debug/common/debugUtils.js'; @@ -566,16 +566,13 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I } public async $substituteVariables(folderUri: UriComponents | undefined, config: IConfig): Promise { - let ws: IWorkspaceFolder | undefined; + let ws: IWorkspaceFolderData | undefined; const folder = await this.getFolder(folderUri); if (folder) { ws = { uri: folder.uri, name: folder.name, index: folder.index, - toResource: () => { - throw new Error('Not implemented'); - } }; } const variableResolver = await this._variableResolver.getResolver(); diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 92151241e98..a2a2f8377ce 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -7,6 +7,7 @@ import type * as vscode from 'vscode'; import { raceCancellation } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { CancellationError } from '../../../base/common/errors.js'; +import { Lazy } from '../../../base/common/lazy.js'; import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { revive } from '../../../base/common/marshalling.js'; import { generateUuid } from '../../../base/common/uuid.js'; @@ -14,13 +15,12 @@ import { IExtensionDescription } from '../../../platform/extensions/common/exten import { IPreparedToolInvocation, isToolInvocationContext, IToolInvocation, IToolInvocationContext, IToolInvocationPreparationContext, IToolResult, ToolInvocationPresentation } 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 { SearchExtensionsToolId } from '../../contrib/extensions/common/searchExtensionsTool.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { Dto, SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js'; import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import { ExtHostLanguageModels } from './extHostLanguageModels.js'; import * as typeConvert from './extHostTypeConverters.js'; -import { SearchExtensionsToolId } from '../../contrib/extensions/common/searchExtensionsTool.js'; -import { Lazy } from '../../../base/common/lazy.js'; class Tool { @@ -125,6 +125,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape context: options.toolInvocationToken as IToolInvocationContext | undefined, chatRequestId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatRequestId : undefined, chatInteractionId: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.chatInteractionId : undefined, + fromSubAgent: isProposedApiEnabled(extension, 'chatParticipantPrivate') ? options.fromSubAgent : undefined, }, token); const dto: Dto = result instanceof SerializableObjectWithBuffers ? result.value : result; @@ -184,6 +185,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape options.chatRequestId = dto.chatRequestId; options.chatInteractionId = dto.chatInteractionId; options.chatSessionId = dto.context?.sessionId; + options.fromSubAgent = dto.fromSubAgent; } if (isProposedApiEnabled(item.extension, 'chatParticipantAdditions') && dto.modelId) { diff --git a/src/vs/workbench/api/common/extHostLanguageModels.ts b/src/vs/workbench/api/common/extHostLanguageModels.ts index 074bd916f43..e94dac6acb0 100644 --- a/src/vs/workbench/api/common/extHostLanguageModels.ts +++ b/src/vs/workbench/api/common/extHostLanguageModels.ts @@ -172,7 +172,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { this._clearModelCache(vendor); // TODO @lramos15 - Remove this old prepare method support in debt week const modelInformation: vscode.LanguageModelChatInformation[] = (data.provider.provideLanguageModelChatInformation ? await data.provider.provideLanguageModelChatInformation(options, token) : await (data.provider as any).prepareLanguageModelChatInformation(options, token)) ?? []; - const modelMetadataAndIdentifier: ILanguageModelChatMetadataAndIdentifier[] = modelInformation.map(m => { + const modelMetadataAndIdentifier: ILanguageModelChatMetadataAndIdentifier[] = modelInformation.map((m): ILanguageModelChatMetadataAndIdentifier => { let auth; if (m.requiresAuthorization && isProposedApiEnabled(data.extension, 'chatProvider')) { auth = { @@ -180,6 +180,10 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { accountLabel: typeof m.requiresAuthorization === 'object' ? m.requiresAuthorization.label : undefined }; } + if (m.capabilities.editTools) { + checkProposedApiEnabled(data.extension, 'chatProvider'); + } + return { metadata: { extension: data.extension.identifier, @@ -199,6 +203,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { modelPickerCategory: m.category ?? DEFAULT_MODEL_PICKER_CATEGORY, capabilities: m.capabilities ? { vision: m.capabilities.imageInput, + editTools: m.capabilities.editTools, toolCalling: !!m.capabilities.toolCalling, agentMode: !!m.capabilities.toolCalling } : undefined, @@ -360,6 +365,7 @@ export class ExtHostLanguageModels implements ExtHostLanguageModelsShape { capabilities: { supportsImageToText: model.metadata.capabilities?.vision ?? false, supportsToolCalling: !!model.metadata.capabilities?.toolCalling, + editToolsHint: model.metadata.capabilities?.editTools, }, maxInputTokens: model.metadata.maxInputTokens, countTokens(text, token) { diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index e01b1df9f1a..f4232eec7bd 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -6,22 +6,24 @@ import * as vscode from 'vscode'; import { DeferredPromise, raceCancellationError, Sequencer, timeout } from '../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; +import { CancellationError } from '../../../base/common/errors.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; +import { AUTH_SERVER_METADATA_DISCOVERY_PATH, getDefaultMetadataForUrl, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, isAuthorizationProtectedResourceMetadata, isAuthorizationServerMetadata, OPENID_CONNECT_DISCOVERY_PATH, parseWWWAuthenticateHeader } from '../../../base/common/oauth.js'; import { SSEParser } from '../../../base/common/sseParser.js'; +import { URI, UriComponents } from '../../../base/common/uri.js'; +import { ConfigurationTarget } from '../../../platform/configuration/common/configuration.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { canLog, ILogService, LogLevel } from '../../../platform/log/common/log.js'; import { StorageScope } from '../../../platform/storage/common/storage.js'; import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportHTTP, McpServerTransportType, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js'; -import { ExtHostMcpShape, MainContext, MainThreadMcpShape } from './extHost.protocol.js'; +import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js'; +import { ExtHostMcpShape, IStartMcpOptions, MainContext, MainThreadMcpShape } from './extHost.protocol.js'; +import { IExtHostInitDataService } from './extHostInitDataService.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import * as Convert from './extHostTypeConverters.js'; -import { AUTH_SERVER_METADATA_DISCOVERY_PATH, OPENID_CONNECT_DISCOVERY_PATH, getDefaultMetadataForUrl, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, isAuthorizationProtectedResourceMetadata, isAuthorizationServerMetadata, parseWWWAuthenticateHeader } from '../../../base/common/oauth.js'; -import { URI } from '../../../base/common/uri.js'; -import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js'; -import { CancellationError } from '../../../base/common/errors.js'; -import { ConfigurationTarget } from '../../../platform/configuration/common/configuration.js'; -import { IExtHostInitDataService } from './extHostInitDataService.js'; +import { IExtHostVariableResolverProvider } from './extHostVariableResolverService.js'; +import { IExtHostWorkspace } from './extHostWorkspace.js'; export const IExtHostMpcService = createDecorator('IExtHostMpcService'); @@ -32,7 +34,7 @@ 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()); + protected readonly _sseEventSources = this._register(new DisposableMap()); private readonly _unresolvedMcpServers = new Map(_workspaceFolder: UriComponents | undefined, value: T): Promise { + const folderURI = URI.revive(_workspaceFolder); + const folder = folderURI && await this._workspaceService.resolveWorkspaceFolder(folderURI); + const variableResolver = await this._variableResolver.getResolver(); + return variableResolver.resolveAsync(folder && { + uri: folder.uri, + name: folder.name, + index: folder.index, + }, value) as T; + } + $stopMcp(id: number): void { if (this._sseEventSources.has(id)) { this._sseEventSources.deleteAndDispose(id); @@ -186,7 +201,7 @@ const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; * 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 { +export class McpHTTPHandle extends Disposable { private readonly _requestSequencer = new Sequencer(); private readonly _postEndpoint = new DeferredPromise<{ url: string; transport: McpServerTransportHTTP }>(); private _mode: HttpModeT = { value: HttpMode.Unknown }; @@ -315,7 +330,7 @@ class McpHTTPHandle extends Disposable { } } - private async _populateAuthMetadata(mcpUrl: string, originalResponse: Response): Promise { + private async _populateAuthMetadata(mcpUrl: string, originalResponse: CommonResponse): Promise { // If there is a resource_metadata challenge, use that to get the oauth server. This is done in 2 steps. // First, extract the resource_metada challenge from the WWW-Authenticate header (if available) let resourceMetadataChallenge: string | undefined; @@ -471,7 +486,7 @@ class McpHTTPHandle extends Disposable { throw new Error(`Invalid authorization server metadata: ${JSON.stringify(body)}`); } - private async _handleSuccessfulStreamableHttp(res: Response, message: string) { + private async _handleSuccessfulStreamableHttp(res: CommonResponse, message: string) { if (res.status === 202) { return; // no body } @@ -516,7 +531,7 @@ class McpHTTPHandle extends Disposable { for (let retry = 0; !this._store.isDisposed; retry++) { await timeout(Math.min(retry * 1000, 30_000), this._cts.token); - let res: Response; + let res: CommonResponse; try { const headers: Record = { ...Object.fromEntries(this._launch.headers), @@ -584,7 +599,7 @@ class McpHTTPHandle extends Disposable { }; await this._addAuthHeader(headers); - let res: Response; + let res: CommonResponse; try { res = await this._fetchWithAuthRetry( this._launch.uri.toString(true), @@ -643,7 +658,7 @@ class McpHTTPHandle extends Disposable { } /** Generic handle to pipe a response into an SSE parser. */ - private async _doSSE(parser: SSEParser, res: Response) { + private async _doSSE(parser: SSEParser, res: CommonResponse) { if (!res.body) { return; } @@ -692,7 +707,7 @@ class McpHTTPHandle extends Disposable { } } - private async _getErrText(res: Response) { + private async _getErrText(res: CommonResponse) { try { return await res.text(); } catch { @@ -705,7 +720,7 @@ class McpHTTPHandle extends Disposable { * If the initial request returns 401 and we don't have auth metadata, * it will populate the auth metadata and retry once. */ - private async _fetchWithAuthRetry(mcpUrl: string, init: MinimalRequestInit, headers: Record): Promise { + private async _fetchWithAuthRetry(mcpUrl: string, init: MinimalRequestInit, headers: Record): Promise { const doFetch = () => this._fetch(mcpUrl, init); let res = await doFetch(); @@ -723,7 +738,7 @@ class McpHTTPHandle extends Disposable { return res; } - private async _fetch(url: string, init: MinimalRequestInit): Promise { + private async _fetch(url: string, init: MinimalRequestInit): Promise { if (canLog(this._logService.getLevel(), LogLevel.Trace)) { const traceObj: any = { ...init, headers: { ...init.headers } }; if (traceObj.body) { @@ -736,9 +751,9 @@ class McpHTTPHandle extends Disposable { } let currentUrl = url; - let response!: Response; + let response!: CommonResponse; for (let redirectCount = 0; redirectCount < MAX_FOLLOW_REDIRECTS; redirectCount++) { - response = await fetch(currentUrl, { + response = await this._fetchInternal(currentUrl, { ...init, signal: this._abortCtrl.signal, redirect: 'manual' @@ -775,6 +790,10 @@ class McpHTTPHandle extends Disposable { return response; } + + protected _fetchInternal(url: string, init?: CommonRequestInit): Promise { + return fetch(url, init); + } } interface MinimalRequestInit { @@ -783,6 +802,21 @@ interface MinimalRequestInit { body?: Uint8Array; } +export interface CommonRequestInit extends MinimalRequestInit { + signal?: AbortSignal; + redirect?: RequestRedirect; +} + +export interface CommonResponse { + status: number; + statusText: string; + headers: Headers; + body?: ReadableStream | null; + url: string; + json(): Promise; + text(): Promise; +} + function isJSON(str: string): boolean { try { JSON.parse(str); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 4e6f9cc9b86..9c3859feb4c 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -41,6 +41,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; import { IViewBadge } from '../../common/views.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; +import { IChatRequestModeInstructions } from '../../contrib/chat/common/chatModel.js'; import { IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatMultiDiffData, IChatPrepareToolInvocationPart, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatThinkingPart, IChatToolInvocationSerialized, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; import { IChatRequestVariableEntry, isImageVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry } from '../../contrib/chat/common/chatVariableEntries.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; @@ -2802,7 +2803,8 @@ export namespace ChatToolInvocationPart { source: ToolDataSource.External, // isError: part.isError ?? false, toolSpecificData: part.toolSpecificData ? convertToolSpecificData(part.toolSpecificData) : undefined, - presentation: undefined + presentation: undefined, + fromSubAgent: part.fromSubAgent }; } @@ -2851,6 +2853,7 @@ export namespace ChatToolInvocationPart { if (part.toolSpecificData) { toolInvocation.toolSpecificData = convertFromInternalToolSpecificData(part.toolSpecificData); } + toolInvocation.fromSubAgent = part.fromSubAgent; return toolInvocation; } @@ -3129,7 +3132,7 @@ export namespace ChatAgentRequest { model, editedFileEvents: request.editedFileEvents, modeInstructions: request.modeInstructions?.content, - modeInstructionsToolReferences: request.modeInstructions?.toolReferences?.map(ChatLanguageModelToolReference.to), + modeInstructions2: ChatRequestModeInstructions.to(request.modeInstructions), }; if (!isProposedApiEnabled(extension, 'chatParticipantPrivate')) { @@ -3259,6 +3262,16 @@ export namespace ChatLanguageModelToolReference { } } +export namespace ChatRequestModeInstructions { + export function to(mode: IChatRequestModeInstructions | undefined): vscode.ChatRequestModeInstructions | undefined { + return mode ? { + content: mode.content, + toolReferences: mode.toolReferences?.map(ref => ChatLanguageModelToolReference.to(ref)) ?? [], + metadata: mode.metadata + } : undefined; + } +} + export namespace ChatAgentCompletionItem { export function from(item: vscode.ChatCompletionItem, commandsConverter: CommandsConverter, disposables: DisposableStore): extHostProtocol.IChatAgentCompletionItem { return { @@ -3550,7 +3563,7 @@ export namespace LanguageModelToolResult { export namespace LanguageModelToolResult2 { export function to(result: IToolResult): vscode.LanguageModelToolResult2 { - return new types.LanguageModelToolResult2(result.content.map(item => { + const toolResult = new types.LanguageModelToolResult2(result.content.map(item => { if (item.kind === 'text') { return new types.LanguageModelTextPart(item.value, item.audience); } else if (item.kind === 'data') { @@ -3559,6 +3572,12 @@ export namespace LanguageModelToolResult2 { return new types.LanguageModelPromptTsxPart(item.value); } })); + + if (result.toolMetadata) { + (toolResult as vscode.ExtendedLanguageModelToolResult).toolMetadata = result.toolMetadata; + } + + return toolResult; } export function from(result: vscode.ExtendedLanguageModelToolResult2, extension: IExtensionDescription): Dto | SerializableObjectWithBuffers> { @@ -3622,6 +3641,7 @@ export namespace LanguageModelToolResult2 { }), toolResultMessage: MarkdownString.fromStrict(result.toolResultMessage), toolResultDetails: detailsDto, + toolMetadata: result.toolMetadata, }; return hasBuffers ? new SerializableObjectWithBuffers(dto) : dto; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 4266842ffa7..0ff82572925 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -21,37 +21,34 @@ import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/ex import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from '../../../platform/files/common/files.js'; import { RemoteAuthorityResolverErrorCode } from '../../../platform/remote/common/remoteAuthorityResolver.js'; import { IRelativePatternDto } from './extHost.protocol.js'; -import { Position } from './extHostTypes/position.js'; -import { es5ClassCompat } from './extHostTypes/es5ClassCompat.js'; -import { Range } from './extHostTypes/range.js'; import { CodeActionKind } from './extHostTypes/codeActionKind.js'; -import { Location } from './extHostTypes/location.js'; import { Diagnostic } from './extHostTypes/diagnostic.js'; +import { es5ClassCompat } from './extHostTypes/es5ClassCompat.js'; +import { Location } from './extHostTypes/location.js'; +import { MarkdownString } from './extHostTypes/markdownString.js'; +import { Position } from './extHostTypes/position.js'; +import { Range } from './extHostTypes/range.js'; +import { SnippetString } from './extHostTypes/snippetString.js'; +import { SymbolKind, SymbolTag } from './extHostTypes/symbolInformation.js'; import { TextEdit } from './extHostTypes/textEdit.js'; import { WorkspaceEdit } from './extHostTypes/workspaceEdit.js'; -import { SnippetString } from './extHostTypes/snippetString.js'; -import { MarkdownString } from './extHostTypes/markdownString.js'; -import { SymbolKind, SymbolTag } from './extHostTypes/symbolInformation.js'; -export { Position } from './extHostTypes/position.js'; -export { Range } from './extHostTypes/range.js'; -export { Selection } from './extHostTypes/selection.js'; export { CodeActionKind } from './extHostTypes/codeActionKind.js'; -export { Location } from './extHostTypes/location.js'; export { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag } from './extHostTypes/diagnostic.js'; -export { EndOfLine, TextEdit } from './extHostTypes/textEdit.js'; -export { FileEditType, WorkspaceEdit } from './extHostTypes/workspaceEdit.js'; +export { Location } from './extHostTypes/location.js'; +export { MarkdownString } from './extHostTypes/markdownString.js'; +export { NotebookCellData, NotebookCellKind, NotebookCellOutput, NotebookCellOutputItem, NotebookData, NotebookEdit, NotebookRange } from './extHostTypes/notebooks.js'; +export { Position } from './extHostTypes/position.js'; +export { Range } from './extHostTypes/range.js'; +export { Selection } from './extHostTypes/selection.js'; export { SnippetString } from './extHostTypes/snippetString.js'; export { SnippetTextEdit } from './extHostTypes/snippetTextEdit.js'; -export { - NotebookCellKind, NotebookRange, NotebookCellData, NotebookCellOutput, - NotebookData, NotebookEdit, NotebookCellOutputItem -} from './extHostTypes/notebooks.js'; -export { MarkdownString } from './extHostTypes/markdownString.js'; -export { SymbolKind, SymbolTag, SymbolInformation } from './extHostTypes/symbolInformation.js'; +export { SymbolInformation, SymbolKind, SymbolTag } from './extHostTypes/symbolInformation.js'; +export { EndOfLine, TextEdit } from './extHostTypes/textEdit.js'; +export { FileEditType, WorkspaceEdit } from './extHostTypes/workspaceEdit.js'; export enum TerminalOutputAnchor { Top = 0, @@ -3354,6 +3351,7 @@ export class ChatToolInvocationPart { isConfirmed?: boolean; isComplete?: boolean; toolSpecificData?: ChatTerminalToolInvocationData2; + fromSubAgent?: boolean; constructor(toolName: string, toolCallId: string, @@ -3790,6 +3788,9 @@ export class LanguageModelToolResult2 { } export class ExtendedLanguageModelToolResult extends LanguageModelToolResult { + toolResultMessage?: string | MarkdownString; + toolResultDetails?: Array; + toolMetadata?: unknown; } export enum LanguageModelChatToolMode { diff --git a/src/vs/workbench/api/node/extHostMcpNode.ts b/src/vs/workbench/api/node/extHostMcpNode.ts index a4e351d4b92..68430e044d0 100644 --- a/src/vs/workbench/api/node/extHostMcpNode.ts +++ b/src/vs/workbench/api/node/extHostMcpNode.ts @@ -6,35 +6,30 @@ import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; import { readFile } from 'fs/promises'; import { homedir } from 'os'; +import type { RequestInit as UndiciRequestInit } from 'undici'; import { parseEnvFile } from '../../../base/common/envfile.js'; import { untildify } from '../../../base/common/labels.js'; +import { Lazy } from '../../../base/common/lazy.js'; import { DisposableMap } from '../../../base/common/lifecycle.js'; import * as path from '../../../base/common/path.js'; +import { URI } from '../../../base/common/uri.js'; import { StreamSplitter } from '../../../base/node/nodeStreams.js'; import { findExecutable } from '../../../base/node/processes.js'; -import { ILogService, LogLevel } from '../../../platform/log/common/log.js'; +import { LogLevel } from '../../../platform/log/common/log.js'; import { McpConnectionState, McpServerLaunch, McpServerTransportStdio, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import { McpStdioStateHandler } from '../../contrib/mcp/node/mcpStdioStateHandler.js'; -import { IExtHostInitDataService } from '../common/extHostInitDataService.js'; -import { ExtHostMcpService } from '../common/extHostMcp.js'; -import { IExtHostRpcService } from '../common/extHostRpcService.js'; +import { CommonRequestInit, CommonResponse, ExtHostMcpService, McpHTTPHandle } from '../common/extHostMcp.js'; export class NodeExtHostMpcService extends ExtHostMcpService { - constructor( - @IExtHostRpcService extHostRpc: IExtHostRpcService, - @IExtHostInitDataService initDataService: IExtHostInitDataService, - @ILogService logService: ILogService, - ) { - super(extHostRpc, logService, initDataService); - } - private nodeServers = this._register(new DisposableMap()); - protected override _startMcp(id: number, launch: McpServerLaunch, errorOnUserInteraction?: boolean): void { + protected override _startMcp(id: number, launch: McpServerLaunch, defaultCwd?: URI, errorOnUserInteraction?: boolean): void { if (launch.type === McpServerTransportType.Stdio) { - this.startNodeMpc(id, launch); + this.startNodeMpc(id, launch, defaultCwd); + } else if (launch.type === McpServerTransportType.HTTP) { + this._sseEventSources.set(id, new McpHTTPHandleNode(id, launch, this._proxy, this._logService, errorOnUserInteraction)); } else { - super._startMcp(id, launch, errorOnUserInteraction); + super._startMcp(id, launch, defaultCwd, errorOnUserInteraction); } } @@ -56,7 +51,7 @@ export class NodeExtHostMpcService extends ExtHostMcpService { } } - private async startNodeMpc(id: number, launch: McpServerTransportStdio) { + private async startNodeMpc(id: number, launch: McpServerTransportStdio, defaultCwd?: URI): Promise { const onError = (err: Error | string) => this._proxy.$onDidChangeState(id, { state: McpConnectionState.Kind.Error, code: err.hasOwnProperty('code') ? String((err as any).code) : undefined, @@ -85,7 +80,7 @@ export class NodeExtHostMpcService extends ExtHostMcpService { const home = homedir(); let cwd = launch.cwd ? untildify(launch.cwd, home) : home; if (!path.isAbsolute(cwd)) { - cwd = path.join(home, cwd); + cwd = defaultCwd ? path.join(defaultCwd.fsPath, cwd) : path.join(home, cwd); } const { executable, args, shell } = await formatSubprocessArguments( @@ -143,6 +138,47 @@ export class NodeExtHostMpcService extends ExtHostMcpService { } } +class McpHTTPHandleNode extends McpHTTPHandle { + private readonly _undici = new Lazy(() => import('undici')); + + protected override async _fetchInternal(url: string, init?: CommonRequestInit): Promise { + // Note: imported async so that we can ensure we load undici after proxy patches have been applied + const { fetch, Agent } = await this._undici.value; + + const undiciInit: UndiciRequestInit = { ...init }; + + let httpUrl = url; + const uri = URI.parse(url); + + if (uri.scheme === 'unix' || uri.scheme === 'pipe') { + // By convention, we put the *socket path* as the URI path, and the *request path* in the fragment + // So, set the dispatcher with the socket path + undiciInit.dispatcher = new Agent({ + socketPath: uri.path, + }); + + // And then rewrite the URL to be http://localhost/ + httpUrl = uri.with({ + scheme: 'http', + authority: 'localhost', // HTTP always wants a host (not that we're using it), but if we're using a socket or pipe then localhost is sorta right anyway + path: uri.fragment, + }).toString(true); + } + + const undiciResponse = await fetch(httpUrl, undiciInit); + + return { + status: undiciResponse.status, + statusText: undiciResponse.statusText, + headers: undiciResponse.headers, + body: undiciResponse.body as ReadableStream, // Way down in `ReadableStreamReadDoneResult`, `value` is optional in the undici type but required (yet can be `undefined`) in the standard type + url: undiciResponse.url, + json: () => undiciResponse.json(), + text: () => undiciResponse.text(), + }; + } +} + const windowsShellScriptRe = /\.(bat|cmd)$/i; /** diff --git a/src/vs/workbench/api/node/extensionHostProcess.ts b/src/vs/workbench/api/node/extensionHostProcess.ts index c754847d13f..4dfcfbe3b3d 100644 --- a/src/vs/workbench/api/node/extensionHostProcess.ts +++ b/src/vs/workbench/api/node/extensionHostProcess.ts @@ -21,7 +21,7 @@ import { boolean } from '../../../editor/common/config/editorOptions.js'; import product from '../../../platform/product/common/product.js'; import { ExtensionHostMain, IExitFn } from '../common/extensionHostMain.js'; import { IHostUtils } from '../common/extHostExtensionService.js'; -import { createURITransformer } from './uriTransformer.js'; +import { createURITransformer } from '../../../base/common/uriTransformer.js'; import { ExtHostConnectionType, readExtHostConnection } from '../../services/extensions/common/extensionHostEnv.js'; import { ExtensionHostExitCode, IExtHostReadyMessage, IExtHostReduceGraceTimeMessage, IExtHostSocketMessage, IExtensionHostInitData, MessageType, createMessageOfType, isMessageOfType } from '../../services/extensions/common/extensionHostProtocol.js'; import { IDisposable } from '../../../base/common/lifecycle.js'; diff --git a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts index 0fcbee571b3..8457e13184e 100644 --- a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts @@ -387,7 +387,6 @@ suite('MainThreadChatSessions', function () { // Valid const chatSessionItem = await chatSessionsService.provideNewChatSessionItem('test-type', { request: mockRequest, - prompt: 'my prompt', metadata: {} }, CancellationToken.None); assert.strictEqual(chatSessionItem.id, 'new-session-id'); @@ -397,7 +396,6 @@ suite('MainThreadChatSessions', function () { await assert.rejects( chatSessionsService.provideNewChatSessionItem('invalid-type', { request: mockRequest, - prompt: 'my prompt', metadata: {} }, CancellationToken.None) ); diff --git a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts index d7a3c6f68e8..36ad00f99db 100644 --- a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts +++ b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts @@ -279,7 +279,7 @@ suite('ExtensionsActivator', () => { const basicActivationEventsReader: IActivationEventsReader = { readActivationEvents: (extensionDescription: IExtensionDescription): string[] => { - return extensionDescription.activationEvents ?? []; + return extensionDescription.activationEvents?.slice() ?? []; } }; diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 7ad8b4b6ec7..1da0210c75d 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -3,12 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from '../../base/common/event.js'; -import { Disposable, DisposableStore, MutableDisposable } from '../../base/common/lifecycle.js'; +import { Disposable } from '../../base/common/lifecycle.js'; import { IContextKeyService, IContextKey, setConstant as setConstantContextKey } from '../../platform/contextkey/common/contextkey.js'; -import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from '../../platform/contextkey/common/contextkeys.js'; +import { IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext, IsIOSContext, ProductQualityContext, IsMobileContext } from '../../platform/contextkey/common/contextkeys.js'; import { SplitEditorsVertically, InEditorZenModeContext, AuxiliaryBarVisibleContext, SideBarVisibleContext, PanelAlignmentContext, PanelMaximizedContext, PanelVisibleContext, EmbedderIdentifierContext, EditorTabsVisibleContext, IsMainEditorCenteredLayoutContext, MainEditorAreaVisibleContext, DirtyWorkingCopiesContext, EmptyWorkspaceSupportContext, EnterMultiRootWorkspaceSupportContext, HasWebFileSystemAccess, IsMainWindowFullscreenContext, OpenFolderWorkspaceSupportContext, RemoteNameContext, VirtualWorkspaceContext, WorkbenchStateContext, WorkspaceFolderCountContext, PanelPositionContext, TemporaryWorkspaceContext, TitleBarVisibleContext, TitleBarStyleContext, IsAuxiliaryWindowFocusedContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorGroupLockedContext, MultipleEditorGroupsContext, EditorsVisibleContext, AuxiliaryBarMaximizedContext, InAutomationContext } from '../common/contextkeys.js'; -import { trackFocus, addDisposableListener, EventType, onDidRegisterWindow, getActiveWindow, isEditableElement } from '../../base/browser/dom.js'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from '../services/editor/common/editorGroupsService.js'; import { IConfigurationService } from '../../platform/configuration/common/configuration.js'; import { IWorkbenchEnvironmentService } from '../services/environment/common/environmentService.js'; @@ -27,7 +25,6 @@ import { isFullscreen, onDidChangeFullscreen } from '../../base/browser/browser. import { IEditorService } from '../services/editor/common/editorService.js'; export class WorkbenchContextKeysHandler extends Disposable { - private inputFocusedContext: IContextKey; private dirtyWorkingCopiesContext: IContextKey; @@ -128,9 +125,6 @@ export class WorkbenchContextKeysHandler extends Disposable { this.dirtyWorkingCopiesContext = DirtyWorkingCopiesContext.bindTo(this.contextKeyService); this.dirtyWorkingCopiesContext.set(this.workingCopyService.hasDirty); - // Inputs - this.inputFocusedContext = InputFocusedContext.bindTo(this.contextKeyService); - // Workbench State this.workbenchStateContext = WorkbenchStateContext.bindTo(this.contextKeyService); this.updateWorkbenchStateContextKey(); @@ -224,14 +218,6 @@ export class WorkbenchContextKeysHandler extends Disposable { this._register(this.editorGroupService.onDidChangeEditorPartOptions(() => this.updateEditorAreaContextKeys())); - this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => { - const onFocusDisposables = disposables.add(new MutableDisposable()); - disposables.add(addDisposableListener(window, EventType.FOCUS_IN, () => { - onFocusDisposables.value = new DisposableStore(); - this.updateInputContextKeys(window.document, onFocusDisposables.value); - }, true)); - }, { window: mainWindow, disposables: this._store })); - this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateWorkbenchStateContextKey())); this._register(this.contextService.onDidChangeWorkspaceFolders(() => { this.updateWorkspaceFolderCountContextKey(); @@ -316,36 +302,6 @@ export class WorkbenchContextKeysHandler extends Disposable { this.editorTabsVisibleContext.set(this.editorGroupService.partOptions.showTabs === 'multiple'); } - private updateInputContextKeys(ownerDocument: Document, disposables: DisposableStore): void { - - function activeElementIsInput(): boolean { - return !!ownerDocument.activeElement && isEditableElement(ownerDocument.activeElement); - } - - const isInputFocused = activeElementIsInput(); - this.inputFocusedContext.set(isInputFocused); - - if (isInputFocused) { - const tracker = disposables.add(trackFocus(ownerDocument.activeElement as HTMLElement)); - Event.once(tracker.onDidBlur)(() => { - - // Ensure we are only updating the context key if we are - // still in the same document that we are tracking. This - // fixes a race condition in multi-window setups where - // the blur event arrives in the inactive window overwriting - // the context key of the active window. This is because - // blur events from the focus tracker are emitted with a - // timeout of 0. - - if (getActiveWindow().document === ownerDocument) { - this.inputFocusedContext.set(activeElementIsInput()); - } - - tracker.dispose(); - }, undefined, disposables); - } - } - private updateWorkbenchStateContextKey(): void { this.workbenchStateContext.set(this.getWorkbenchStateString()); } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index bc61f85418f..0d7205a2973 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -632,7 +632,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private initLayoutState(lifecycleService: ILifecycleService, fileService: IFileService): void { this._mainContainerDimension = getClientArea(this.parent, this.contextService.getWorkbenchState() === WorkbenchState.EMPTY ? DEFAULT_EMPTY_WINDOW_DIMENSIONS : DEFAULT_WORKSPACE_WINDOW_DIMENSIONS); // running with fallback to ensure no error is thrown (https://github.com/microsoft/vscode/issues/240242) - this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService); + this.stateModel = new LayoutStateModel(this.storageService, this.configurationService, this.contextService, this.environmentService); this.stateModel.load({ mainContainerDimension: this._mainContainerDimension, resetLayout: Boolean(this.layoutOptions?.resetLayout) @@ -2789,6 +2789,7 @@ class LayoutStateModel extends Disposable { private readonly storageService: IStorageService, private readonly configurationService: IConfigurationService, private readonly contextService: IWorkspaceContextService, + private readonly environmentService: IBrowserWorkbenchEnvironmentService, ) { super(); @@ -2857,6 +2858,10 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = workbenchState === WorkbenchState.EMPTY; LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, mainContainerDimension.width / 4); LayoutStateKeys.AUXILIARYBAR_HIDDEN.defaultValue = (() => { + if (isWeb && !this.environmentService.remoteAuthority) { + return true; // TODO@bpasero remove this condition once Chat web support lands + } + const configuration = this.configurationService.inspect(WorkbenchLayoutSettings.AUXILIARYBAR_DEFAULT_VISIBILITY); // Unless auxiliary bar visibility is explicitly configured, make diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index a9a31656a8b..4c9086f8df8 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -181,13 +181,13 @@ export abstract class CompositePart extends Part { const compositeDescriptor = this.registry.getComposite(id); if (compositeDescriptor) { const that = this; - const compositeProgressIndicator = new ScopedProgressIndicator(assertReturnsDefined(this.progressBar), new class extends AbstractProgressScope { + const compositeProgressIndicator = new ScopedProgressIndicator(assertReturnsDefined(this.progressBar), this._register(new class extends AbstractProgressScope { constructor() { super(compositeDescriptor!.id, !!isActive); this._register(that.onDidCompositeOpen.event(e => this.onScopeOpened(e.composite.getId()))); this._register(that.onDidCompositeClose.event(e => this.onScopeClosed(e.getId()))); } - }()); + }())); const compositeInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection( [IEditorProgressService, compositeProgressIndicator] // provide the editor progress service for any editors instantiated within the composite ))); diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 9157f122f09..7fdea24ffcd 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -59,8 +59,6 @@ import { inQuickPickContext, getQuickNavigateHandler } from '../../quickaccess.j import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; import { isMacintosh } from '../../../../base/common/platform.js'; -import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import { FloatingEditorClickMenu } from '../../codeeditor.js'; import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { EditorAutoSave } from './editorAutoSave.js'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from '../../../../platform/quickinput/common/quickAccess.js'; @@ -136,8 +134,6 @@ registerWorkbenchContribution2(EditorStatusContribution.ID, EditorStatusContribu registerWorkbenchContribution2(UntitledTextEditorWorkingCopyEditorHandler.ID, UntitledTextEditorWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(DynamicEditorConfigurations.ID, DynamicEditorConfigurations, WorkbenchPhase.BlockRestore); -registerEditorContribution(FloatingEditorClickMenu.ID, FloatingEditorClickMenu, EditorContributionInstantiation.AfterFirstRender); - //#endregion //#region Quick Access diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 51fa5e7bfab..cae1244e405 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -407,7 +407,8 @@ function registerOpenEditorAPICommands(): void { ]; } - // partial, renderer-side API command to open editor + // partial, renderer-side API command to open editor only supporting + // arguments that do not need to be converted from the extension host // complements https://github.com/microsoft/vscode/blob/2b164efb0e6a5de3826bff62683eaeafe032284f/src/vs/workbench/api/common/extHostApiCommands.ts#L373 CommandsRegistry.registerCommand({ id: 'vscode.open', @@ -465,7 +466,8 @@ function registerOpenEditorAPICommands(): void { } }); - // partial, renderer-side API command to open diff editor + // partial, renderer-side API command to open diff editor only supporting + // arguments that do not need to be converted from the extension host // complements https://github.com/microsoft/vscode/blob/2b164efb0e6a5de3826bff62683eaeafe032284f/src/vs/workbench/api/common/extHostApiCommands.ts#L397 CommandsRegistry.registerCommand({ id: 'vscode.diff', @@ -518,7 +520,8 @@ function registerOpenEditorAPICommands(): void { await editorService.openEditor({ resource: URI.from(resource, true), options: { pinned: true, ...optionsArg, override: id } }, columnToEditorGroup(editorGroupsService, configurationService, columnArg)); }); - // partial, renderer-side API command to open diff editor + // partial, renderer-side API command to open diff editor only supporting + // arguments that do not need to be converted from the extension host // complements https://github.com/microsoft/vscode/blob/2b164efb0e6a5de3826bff62683eaeafe032284f/src/vs/workbench/api/common/extHostApiCommands.ts#L397 CommandsRegistry.registerCommand({ id: 'vscode.changes', diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 408d242fbb2..3ff05bb7454 100644 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -142,6 +142,7 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { super.render(container); container.classList.toggle('command-center-quick-pick'); container.role = 'button'; + container.setAttribute('aria-description', this.getTooltip()); const action = this.action; // icon (search) diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 262a5c34bbf..9111b05e6b9 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -298,11 +298,16 @@ } .panel > .title .monaco-action-bar .action-item.viewpane-filter-container { - max-width: 400px; + max-width: 200px; min-width: 150px; margin-right: 10px; } +.panel > .title .monaco-action-bar .action-item.viewpane-filter-container:active, +.panel > .title .monaco-action-bar .action-item.viewpane-filter-container:focus-within { + max-width: 400px; +} + .pane-body .viewpane-filter-container:not(:empty) { flex: 1; margin: 10px 20px; diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index eb1969f1beb..8235fb5dff8 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -613,7 +613,7 @@ export abstract class ViewPane extends Pane implements IView { const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.id); const isDefault = this.viewDescriptorService.getDefaultContainerById(this.id) === viewContainer; - if (!isDefault && viewDescriptor?.containerTitle && model.title !== viewDescriptor.containerTitle) { + if (!isDefault && viewDescriptor?.containerTitle && model.title !== viewDescriptor.containerTitle && title !== viewDescriptor.containerTitle) { return `${viewDescriptor.containerTitle}: ${title}`; } @@ -640,12 +640,12 @@ export abstract class ViewPane extends Pane implements IView { if (this.progressIndicator === undefined) { const that = this; - this.progressIndicator = this._register(new ScopedProgressIndicator(assertReturnsDefined(this.progressBar), new class extends AbstractProgressScope { + this.progressIndicator = this._register(new ScopedProgressIndicator(assertReturnsDefined(this.progressBar), this._register(new class extends AbstractProgressScope { constructor() { super(that.id, that.isBodyVisible()); this._register(that.onDidChangeBodyVisibility(isVisible => isVisible ? this.onScopeOpened(that.id) : this.onScopeClosed(that.id))); } - }())); + }()))); } return this.progressIndicator; } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index ef7e83ed1d0..3424b7e3e93 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -35,7 +35,8 @@ export const enum AccessibilityWorkbenchSettingId { DimUnfocusedEnabled = 'accessibility.dimUnfocused.enabled', DimUnfocusedOpacity = 'accessibility.dimUnfocused.opacity', HideAccessibleView = 'accessibility.hideAccessibleView', - AccessibleViewCloseOnKeyPress = 'accessibility.accessibleView.closeOnKeyPress' + AccessibleViewCloseOnKeyPress = 'accessibility.accessibleView.closeOnKeyPress', + VerboseChatProgressUpdates = 'accessibility.verboseChatProgressUpdates' } export const enum ViewDimUnfocusedOpacityProperties { @@ -843,6 +844,11 @@ export function registerAccessibilityConfiguration() { type: 'boolean', default: false, tags: ['accessibility'] + }, + [AccessibilityWorkbenchSettingId.VerboseChatProgressUpdates]: { + 'type': 'boolean', + 'default': true, + 'markdownDescription': localize('accessibility.verboseChatProgressUpdates', "Controls whether verbose progress announcements should be made when a chat request is in progress, including information like searched text for with X results, created file , or read file .") } } }); diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 21be375a627..8e9cd9ddeaf 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EventType, addDisposableListener, getActiveWindow, isActiveElement } from '../../../../base/browser/dom.js'; +import { EventType, addDisposableListener, getActiveWindow, getWindow, isActiveElement } from '../../../../base/browser/dom.js'; import { IKeyboardEvent, StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { alert } from '../../../../base/browser/ui/aria/aria.js'; @@ -25,6 +25,7 @@ import { IModelService } from '../../../../editor/common/services/model.js'; import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { AccessibilityHelpNLS } from '../../../../editor/common/standaloneStrings.js'; import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; +import { FloatingEditorToolbar } from '../../../../editor/contrib/floatingMenu/browser/floatingMenu.js'; import { localize } from '../../../../nls.js'; import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol, isIAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; import { ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX, IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; @@ -134,7 +135,8 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this._container.classList.add('hide'); } const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { - contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeActionController.ID && c.id !== FloatingEditorClickMenu.ID) + contributions: EditorExtensionsRegistry.getEditorContributions() + .filter(c => c.id !== CodeActionController.ID && c.id !== FloatingEditorClickMenu.ID && c.id !== FloatingEditorToolbar.ID) }; const titleBar = document.createElement('div'); titleBar.classList.add('accessible-view-title-bar'); @@ -629,6 +631,13 @@ export class AccessibleView extends Disposable implements ITextModelContentProvi this._updateToolbar(this._currentProvider.actions, provider.options.type); const hide = (e?: KeyboardEvent | IKeyboardEvent): void => { + const thisWindowIsFocused = getWindow(this._editorWidget.getDomNode()).document.hasFocus(); + if (!thisWindowIsFocused) { + // When switching windows, keep accessible view open + e?.preventDefault(); + e?.stopPropagation(); + return; + } if (!this._isInQuickPick) { provider.onClose(); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 213ecddbb47..d9d262db5d5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -5,7 +5,6 @@ import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; import { AccessibleDiffViewerNext } from '../../../../../editor/browser/widget/diffEditor/commands.js'; import { localize } from '../../../../../nls.js'; import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType } from '../../../../../platform/accessibility/browser/accessibleView.js'; @@ -25,8 +24,7 @@ export class PanelChatAccessibilityHelp implements IAccessibleViewImplementation readonly type = AccessibleViewType.Help; readonly when = ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask), ContextKeyExpr.or(ChatContextKeys.inChatSession, ChatContextKeys.isResponse, ChatContextKeys.isRequest)); getProvider(accessor: ServicesAccessor) { - const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); - return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'panelChat'); + return getChatAccessibilityHelpProvider(accessor, undefined, 'panelChat'); } } @@ -36,8 +34,7 @@ export class QuickChatAccessibilityHelp implements IAccessibleViewImplementation readonly type = AccessibleViewType.Help; readonly when = ContextKeyExpr.and(ChatContextKeys.inQuickChat, ContextKeyExpr.or(ChatContextKeys.inChatSession, ChatContextKeys.isResponse, ChatContextKeys.isRequest)); getProvider(accessor: ServicesAccessor) { - const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); - return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'quickChat'); + return getChatAccessibilityHelpProvider(accessor, undefined, 'quickChat'); } } @@ -47,8 +44,7 @@ export class EditsChatAccessibilityHelp implements IAccessibleViewImplementation readonly type = AccessibleViewType.Help; readonly when = ContextKeyExpr.and(ChatContextKeyExprs.inEditingMode, ChatContextKeys.inChatInput); getProvider(accessor: ServicesAccessor) { - const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); - return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'editsView'); + return getChatAccessibilityHelpProvider(accessor, undefined, 'editsView'); } } @@ -58,28 +54,27 @@ export class AgentChatAccessibilityHelp implements IAccessibleViewImplementation readonly type = AccessibleViewType.Help; readonly when = ContextKeyExpr.and(ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), ChatContextKeys.inChatInput); getProvider(accessor: ServicesAccessor) { - const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor(); - return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'agentView'); + return getChatAccessibilityHelpProvider(accessor, undefined, 'agentView'); } } -export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'quickChat' | 'editsView' | 'agentView', keybindingService: IKeybindingService): string { +export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'agentView' | 'quickChat' | 'editsView' | 'agentView', keybindingService: IKeybindingService): string { const content = []; - if (type === 'panelChat' || type === 'quickChat') { + if (type === 'panelChat' || type === 'quickChat' || type === 'agentView') { if (type === 'quickChat') { content.push(localize('chat.overview', 'The quick chat view is comprised of an input box and a request/response list. The input box is used to make requests and the list is used to display responses.')); content.push(localize('chat.differenceQuick', 'The quick chat view is a transient interface for making and viewing requests, while the panel chat view is a persistent interface that also supports navigating suggested follow-up questions.')); } if (type === 'panelChat') { content.push(localize('chat.differencePanel', 'The panel chat view is a persistent interface that also supports navigating suggested follow-up questions, while the quick chat view is a transient interface for making and viewing requests.')); - content.push(localize('chat.followUp', 'In the input box, navigate to the suggested follow up question (Shift+Tab) and press Enter to run it.')); } content.push(localize('chat.requestHistory', 'In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.')); content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}.', '')); + content.push(localize('workbench.action.chat.focus', 'To focus the chat request and response list, invoke the Focus Chat command{0}. This will move focus to the most recent response, which you can then navigate using the up and down arrow keys.', getChatFocusKeybindingLabel(keybindingService, type, 'last'))); + content.push(localize('workbench.action.chat.focusLastFocusedItem', 'To return to the last chat response you focused, invoke the Focus Last Focused Chat Response command{0}.', getChatFocusKeybindingLabel(keybindingService, type, 'lastFocused'))); + content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command{0}.', getChatFocusKeybindingLabel(keybindingService, type, 'input'))); content.push(localize('chat.progressVerbosity', 'As the chat request is being processed, you will hear verbose progress updates if the request takes more than 4 seconds. This includes information like searched text for with X results, created file , or read file . This can be disabled with accessibility.verboseChatProgressUpdates.')); content.push(localize('chat.announcement', 'Chat responses will be announced as they come in. A response will indicate the number of code blocks, if any, and then the rest of the response.')); - content.push(localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command{0}.', getChatFocusKeybindingLabel(keybindingService, type, false))); - content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command{0}.', getChatFocusKeybindingLabel(keybindingService, type, true))); content.push(localize('workbench.action.chat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command{0}.', '')); content.push(localize('workbench.action.chat.announceConfirmation', 'To focus pending chat confirmation dialogs, invoke the Focus Chat Confirmation Status command{0}.', '')); if (type === 'panelChat') { @@ -134,7 +129,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'qui export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat' | 'quickChat' | 'editsView' | 'agentView'): AccessibleContentProvider | undefined { const widgetService = accessor.get(IChatWidgetService); const keybindingService = accessor.get(IKeybindingService); - const inputEditor: ICodeEditor | undefined = type === 'panelChat' || type === 'editsView' || type === 'quickChat' ? widgetService.lastFocusedWidget?.inputEditor : editor; + const inputEditor: ICodeEditor | undefined = widgetService.lastFocusedWidget?.inputEditor; if (!inputEditor) { return; @@ -152,8 +147,10 @@ export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, edi { type: AccessibleViewType.Help }, () => helpText, () => { - if (type === 'panelChat' && cachedPosition) { - inputEditor.setPosition(cachedPosition); + if (type === 'quickChat' || type === 'editsView' || type === 'agentView' || type === 'panelChat') { + if (cachedPosition) { + inputEditor.setPosition(cachedPosition); + } inputEditor.focus(); } else if (type === 'inlineChat') { @@ -161,9 +158,6 @@ export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, edi const ctrl = <{ focus(): void } | undefined>editor?.getContribution(INLINE_CHAT_ID); ctrl?.focus(); - } else if (type === 'quickChat' || type === 'editsView' || type === 'agentView') { - // For quickChat, editsView, and agentView, restore focus to the chat widget input - widgetService.lastFocusedWidget?.focusInput(); } }, type === 'panelChat' ? AccessibilityVerbositySettingId.Chat : AccessibilityVerbositySettingId.InlineChat, @@ -172,11 +166,13 @@ export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, edi // The when clauses for actions may not be true when we invoke the accessible view, so we need to provide the keybinding label manually // to ensure it's correct -function getChatFocusKeybindingLabel(keybindingService: IKeybindingService, type: 'panelChat' | 'inlineChat' | 'quickChat', focusInput?: boolean): string | undefined { +function getChatFocusKeybindingLabel(keybindingService: IKeybindingService, type: 'agentView' | 'panelChat' | 'inlineChat' | 'quickChat', focus?: 'lastFocused' | 'last' | 'input'): string | undefined { let kbs; const fallback = ' (unassigned keybinding)'; - if (focusInput) { - kbs = keybindingService.lookupKeybindings('workbench.action.chat.focusInput'); + if (focus === 'input') { + kbs = keybindingService.lookupKeybindings('workbench.chat.action.focusInput'); + } else if (focus === 'lastFocused') { + kbs = keybindingService.lookupKeybindings('workbench.chat.action.focusLastFocused'); } else { kbs = keybindingService.lookupKeybindings('chat.action.focus'); } @@ -184,15 +180,15 @@ function getChatFocusKeybindingLabel(keybindingService: IKeybindingService, type return fallback; } let kb; - if (type === 'panelChat') { - if (focusInput) { + if (type === 'agentView' || type === 'panelChat') { + if (focus) { kb = kbs.find(kb => kb.getAriaLabel()?.includes('DownArrow'))?.getAriaLabel(); } else { kb = kbs.find(kb => kb.getAriaLabel()?.includes('UpArrow'))?.getAriaLabel(); } } else { // Quick chat - if (focusInput) { + if (focus) { kb = kbs.find(kb => kb.getAriaLabel()?.includes('UpArrow'))?.getAriaLabel(); } else { kb = kbs.find(kb => kb.getAriaLabel()?.includes('DownArrow'))?.getAriaLabel(); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index f435ad2d688..9b350d949ca 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -522,6 +522,11 @@ export function registerChatActions() { id: MenuId.EditorTitle, when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID), }, + { + id: MenuId.ChatHistory, + when: ChatContextKeys.inEmptyStateWithHistoryEnabled, + group: 'navigation', + } ], category: CHAT_CATEGORY, icon: Codicon.history, @@ -733,6 +738,9 @@ export function registerChatActions() { for (const provider of providers) { const sessions = await chatSessionsService.provideChatSessionItems(provider.type, cancellationToken.token); + if (!sessions?.length) { + continue; + } providerNSessions.push(...sessions.map(session => ({ providerType: provider.type, session }))); } @@ -1321,7 +1329,45 @@ export function registerChatActions() { const editorUri = editor.getModel()?.uri; if (editorUri) { const widgetService = accessor.get(IChatWidgetService); - widgetService.getWidgetByInputUri(editorUri)?.focusLastMessage(); + widgetService.getWidgetByInputUri(editorUri)?.focusResponseItem(); + } + } + }); + + registerAction2(class FocusMostRecentlyFocusedChatAction extends EditorAction2 { + constructor() { + super({ + id: 'workbench.chat.action.focusLastFocused', + title: localize2('actions.interactiveSession.focusLastFocused', 'Focus Last Focused Chat List Item'), + precondition: ContextKeyExpr.and(ChatContextKeys.inChatInput), + category: CHAT_CATEGORY, + keybinding: [ + // On mac, require that the cursor is at the top of the input, to avoid stealing cmd+up to move the cursor to the top + { + when: ContextKeyExpr.and(ChatContextKeys.inputCursorAtTop, ChatContextKeys.inQuickChat.negate()), + primary: KeyMod.CtrlCmd | KeyCode.UpArrow | KeyMod.Shift, + weight: KeybindingWeight.EditorContrib + 1, + }, + // On win/linux, ctrl+up can always focus the chat list + { + when: ContextKeyExpr.and(ContextKeyExpr.or(IsWindowsContext, IsLinuxContext), ChatContextKeys.inQuickChat.negate()), + primary: KeyMod.CtrlCmd | KeyCode.UpArrow | KeyMod.Shift, + weight: KeybindingWeight.EditorContrib + 1, + }, + { + when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inQuickChat), + primary: KeyMod.CtrlCmd | KeyCode.DownArrow | KeyMod.Shift, + weight: KeybindingWeight.WorkbenchContrib + 1, + } + ] + }); + } + + runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise { + const editorUri = editor.getModel()?.uri; + if (editorUri) { + const widgetService = accessor.get(IChatWidgetService); + widgetService.getWidgetByInputUri(editorUri)?.focusResponseItem(true); } } }); @@ -1685,6 +1731,7 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben const chatSentiment = chatEntitlementService.sentiment; const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0; const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown; + const anonymous = chatEntitlementService.anonymous; const free = chatEntitlementService.entitlement === ChatEntitlement.Free; const isAuxiliaryWindow = windowId !== mainWindow.vscodeWindowId; @@ -1692,7 +1739,7 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben let primaryActionTitle = isAuxiliaryWindow ? localize('openChat', "Open Chat") : localize('toggleChat', "Toggle Chat"); let primaryActionIcon = Codicon.chatSparkle; if (chatSentiment.installed && !chatSentiment.disabled) { - if (signedOut) { + if (signedOut && !anonymous) { primaryActionId = CHAT_SETUP_ACTION_ID; primaryActionTitle = localize('signInToChatSetup', "Sign in to use AI features..."); primaryActionIcon = Codicon.chatSparkleError; @@ -1710,7 +1757,8 @@ export class CopilotTitleBarMenuRendering extends Disposable implements IWorkben }, Event.any( chatEntitlementService.onDidChangeSentiment, chatEntitlementService.onDidChangeQuotaExceeded, - chatEntitlementService.onDidChangeEntitlement + chatEntitlementService.onDidChangeEntitlement, + chatEntitlementService.onDidChangeAnonymous )); // Reduces flicker a bit on reload/restart @@ -1979,3 +2027,69 @@ registerAction2(class EditToolApproval extends Action2 { } } }); + +// Register actions for chat welcome history context menu +MenuRegistry.appendMenuItem(MenuId.ChatWelcomeHistoryContext, { + command: { + id: 'workbench.action.chat.toggleChatHistoryVisibility', + title: localize('chat.showChatHistory.label', "✓ Chat History") + }, + group: '1_modify', + order: 1, + when: ContextKeyExpr.equals('chatHistoryVisible', true) +}); + +MenuRegistry.appendMenuItem(MenuId.ChatWelcomeHistoryContext, { + command: { + id: 'workbench.action.chat.toggleChatHistoryVisibility', + title: localize('chat.hideChatHistory.label', "Chat History") + }, + group: '1_modify', + order: 1, + when: ContextKeyExpr.equals('chatHistoryVisible', false) +}); + +registerAction2(class ToggleChatHistoryVisibilityAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.toggleChatHistoryVisibility', + title: localize2('chat.toggleChatHistoryVisibility.label', "Chat History"), + category: CHAT_CATEGORY, + precondition: ChatContextKeys.enabled + }); + } + + async run(accessor: ServicesAccessor): Promise { + const chatWidgetService = accessor.get(IChatWidgetService); + const widgets = chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat); + const widget = widgets?.[0]; + if (widget) { + widget.toggleHistoryVisibility(); + } + } +}); + +registerAction2(class OpenChatEmptyStateSettingsAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.openChatEmptyStateSettings', + title: localize2('chat.openChatEmptyStateSettings.label', "Configure Empty State"), + menu: [ + { + id: MenuId.ChatWelcomeHistoryContext, + group: '2_settings', + order: 1 + } + ], + category: CHAT_CATEGORY, + precondition: ChatContextKeys.enabled + }); + } + + async run(accessor: ServicesAccessor): Promise { + const preferencesService = accessor.get(IPreferencesService); + await preferencesService.openUserSettings({ + query: 'chat.emptyState chat.promptFilesRecommendations' + }); + } +}); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 03c658386af..6ef6dae952b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -188,6 +188,7 @@ export function registerChatCodeBlockActions() { modelId: request?.modelId, presentation: 'codeBlock', applyCodeBlockSuggestionId: undefined, + source: undefined, }); } } @@ -254,6 +255,7 @@ export function registerChatCodeBlockActions() { modelId: request?.modelId, presentation: 'codeBlock', applyCodeBlockSuggestionId: undefined, + source: undefined, }); } @@ -410,6 +412,7 @@ export function registerChatCodeBlockActions() { modelId: request?.modelId, presentation: 'codeBlock', applyCodeBlockSuggestionId: undefined, + source: undefined, }); } } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 262713c22f8..1ea763940e9 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -27,7 +27,7 @@ import { IWorkspaceContextService } from '../../../../../platform/workspace/comm import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IRemoteCodingAgent, IRemoteCodingAgentsService } from '../../../remoteCodingAgents/common/remoteCodingAgentsService.js'; import { IChatAgentHistoryEntry, IChatAgentService } from '../../common/chatAgents.js'; -import { ChatContextKeys } from '../../common/chatContextKeys.js'; +import { ChatContextKeys, ChatContextKeyExprs } from '../../common/chatContextKeys.js'; import { IChatModel, IChatRequestModel, toChatHistoryContent } from '../../common/chatModel.js'; import { IChatMode, IChatModeService } from '../../common/chatModes.js'; import { chatVariableLeader } from '../../common/chatParserTypes.js'; @@ -39,7 +39,7 @@ import { ChatRequestVariableSet, isChatRequestFileEntry } from '../../common/cha import { ChatAgentLocation, ChatConfiguration, ChatModeKind, } from '../../common/constants.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; -import { IChatWidget, IChatWidgetService } from '../chat.js'; +import { IChatWidget, IChatWidgetService, showChatWidgetInViewOrEditor } from '../chat.js'; import { getEditingSessionContext } from '../chatEditing/chatEditingActions.js'; import { IChatEditorOptions } from '../chatEditor.js'; import { ACTION_ID_NEW_CHAT, CHAT_CATEGORY, handleCurrentEditingSession, handleModeSwitch } from './chatActions.js'; @@ -177,7 +177,10 @@ export class ChatSubmitAction extends SubmitAction { tooltip: localize('sendToRemoteAgent', "Send to coding agent"), }, keybinding: { - when: ChatContextKeys.inChatInput, + when: ContextKeyExpr.and( + ChatContextKeys.inChatInput, + ChatContextKeys.withinEditSessionDiff.negate(), + ), primary: KeyCode.Enter, weight: KeybindingWeight.EditorContrib }, @@ -186,7 +189,10 @@ export class ChatSubmitAction extends SubmitAction { id: MenuId.ChatExecuteSecondary, group: 'group_1', order: 1, - when: ContextKeyExpr.and(menuCondition, ChatContextKeys.lockedToCodingAgent.negate()), + when: ContextKeyExpr.or( + ChatContextKeys.withinEditSessionDiff, + ContextKeyExpr.and(menuCondition, ChatContextKeys.lockedToCodingAgent.negate()) + ), }, { id: MenuId.ChatExecute, @@ -194,6 +200,7 @@ export class ChatSubmitAction extends SubmitAction { when: ContextKeyExpr.and( whenNotInProgress, menuCondition, + ChatContextKeys.withinEditSessionDiff.negate(), ), group: 'navigation', }] @@ -201,6 +208,78 @@ export class ChatSubmitAction extends SubmitAction { } } +export class ChatDelegateToEditSessionAction extends Action2 { + static readonly ID = 'workbench.action.chat.delegateToEditSession'; + + constructor() { + super({ + id: ChatDelegateToEditSessionAction.ID, + title: localize2('interactive.submit.panel.label', "Send to Edit Session"), + f1: false, + category: CHAT_CATEGORY, + icon: Codicon.commentDiscussion, + keybinding: { + when: ContextKeyExpr.and( + ChatContextKeys.inChatInput, + ChatContextKeys.withinEditSessionDiff, + ), + primary: KeyCode.Enter, + weight: KeybindingWeight.EditorContrib + }, + menu: [ + { + id: MenuId.ChatExecute, + order: 4, + when: ContextKeyExpr.and( + whenNotInProgress, + ChatContextKeys.withinEditSessionDiff, + ), + group: 'navigation', + }, + { + id: MenuId.ChatExecuteSecondary, + group: 'group_1', + order: 1, + when: ContextKeyExpr.and( + whenNotInProgress, + ChatContextKeys.filePartOfEditSession, + ), + } + ] + }); + } + + override async run(accessor: ServicesAccessor, ...args: any[]): Promise { + const context: IChatExecuteActionContext | undefined = args[0]; + const widgetService = accessor.get(IChatWidgetService); + const instantiationService = accessor.get(IInstantiationService); + const inlineWidget = context?.widget ?? widgetService.lastFocusedWidget; + const locationData = inlineWidget?.locationData; + + if (inlineWidget && locationData?.type === ChatAgentLocation.EditorInline && locationData.delegateSessionId) { + const sessionWidget = widgetService.getWidgetBySessionId(locationData.delegateSessionId); + + if (sessionWidget) { + await instantiationService.invokeFunction(showChatWidgetInViewOrEditor, sessionWidget); + sessionWidget.attachmentModel.addContext({ + id: 'vscode.delegate.inline', + kind: 'file', + modelDescription: `User's chat context`, + name: 'delegate-inline', + value: { range: locationData.wholeRange, uri: locationData.document }, + }); + sessionWidget.acceptInput(inlineWidget.getInput(), { + noCommandDetection: true, + enableImplicitContext: false, + }); + + inlineWidget.setInput(''); + locationData.close(); + } + } + } +} + export const ToggleAgentModeActionId = 'workbench.action.chat.toggleAgentMode'; export interface IToggleChatModeArgs { @@ -382,7 +461,8 @@ export class OpenModePickerAction extends Action2 { ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), - ChatContextKeys.lockedToCodingAgent.negate()), + ChatContextKeys.lockedToCodingAgent.negate(), + ChatContextKeyExprs.chatSetupTriggerContext?.negate()), group: 'navigation', }, ] @@ -585,7 +665,6 @@ export class CreateRemoteAgentJobAction extends Action2 { const newChatSession = await chatSessionsService.provideNewChatSessionItem( type, { - prompt: userPrompt, request: { agentId: '', location: ChatAgentLocation.Chat, @@ -632,7 +711,7 @@ export class CreateRemoteAgentJobAction extends Action2 { // Execute the remote command const result: Omit | string | undefined = await commandService.executeCommand(agent.command, { userPrompt, - summary: summary || userPrompt, + summary, _version: 2, // Signal that we support the new response format }); @@ -1027,6 +1106,7 @@ export class CancelEdit extends Action2 { export function registerChatExecuteActions() { registerAction2(ChatSubmitAction); + registerAction2(ChatDelegateToEditSessionAction); registerAction2(ChatEditingSessionSubmitAction); registerAction2(SubmitWithoutDispatchingAction); registerAction2(CancelAction); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatPromptNavigationActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatPromptNavigationActions.ts new file mode 100644 index 00000000000..e5a3f6c3572 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatPromptNavigationActions.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { localize2 } from '../../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { CHAT_CATEGORY } from './chatActions.js'; +import { IChatWidgetService } from '../chat.js'; +import { ChatContextKeys } from '../../common/chatContextKeys.js'; +import { IChatRequestViewModel, isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; + +export function registerChatPromptNavigationActions() { + registerAction2(class NextUserPromptAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.nextUserPrompt', + title: localize2('interactive.nextUserPrompt.label', "Next User Prompt"), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.RightArrow, + weight: KeybindingWeight.WorkbenchContrib, + when: ChatContextKeys.inChatSession, + }, + precondition: ChatContextKeys.enabled, + f1: true, + category: CHAT_CATEGORY, + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + navigateUserPrompts(accessor, false); + } + }); + + registerAction2(class PreviousUserPromptAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.previousUserPrompt', + title: localize2('interactive.previousUserPrompt.label', "Previous User Prompt"), + keybinding: { + primary: KeyMod.CtrlCmd | KeyCode.LeftArrow, + weight: KeybindingWeight.WorkbenchContrib, + when: ChatContextKeys.inChatSession, + }, + precondition: ChatContextKeys.enabled, + f1: true, + category: CHAT_CATEGORY, + }); + } + + run(accessor: ServicesAccessor, ...args: any[]) { + navigateUserPrompts(accessor, true); + } + }); +} + +function navigateUserPrompts(accessor: ServicesAccessor, reverse: boolean) { + const chatWidgetService = accessor.get(IChatWidgetService); + const widget = chatWidgetService.lastFocusedWidget; + if (!widget) { + return; + } + + const items = widget.viewModel?.getItems(); + if (!items || items.length === 0) { + return; + } + + // Get all user prompts (requests) in the conversation + const userPrompts = items.filter((item): item is IChatRequestViewModel => isRequestVM(item)); + if (userPrompts.length === 0) { + return; + } + + // Find the currently focused item + const focused = widget.getFocus(); + let currentIndex = -1; + + if (focused) { + if (isRequestVM(focused)) { + // If a request is focused, find its index in the user prompts array + currentIndex = userPrompts.findIndex(prompt => prompt.id === focused.id); + } else if (isResponseVM(focused)) { + // If a response is focused, find the associated request's index + // Response view models have a requestId property + currentIndex = userPrompts.findIndex(prompt => prompt.id === focused.requestId); + } + } + + // Calculate next index + let nextIndex: number; + if (currentIndex === -1) { + // No current focus, go to first or last prompt based on direction + nextIndex = reverse ? userPrompts.length - 1 : 0; + } else { + // Navigate to next/previous prompt + nextIndex = reverse ? currentIndex - 1 : currentIndex + 1; + + // Clamp instead of wrap and stay at boundaries when trying to navigate past ends + if (nextIndex < 0) { + nextIndex = 0; // already at first, do not move further + } else if (nextIndex >= userPrompts.length) { + nextIndex = userPrompts.length - 1; // already at last, do not move further + } + + // avoid re-focusing if we didn't actually move + if (nextIndex === currentIndex) { + return; // no change in focus + } + } + + // Focus and reveal the selected user prompt + const targetPrompt = userPrompts[nextIndex]; + if (targetPrompt) { + widget.focus(targetPrompt); + widget.reveal(targetPrompt); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 9dfdbd1ae13..da4b786e881 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -89,6 +89,7 @@ export class InsertCodeBlockOperation { modelId: request?.modelId, presentation: 'codeBlock', applyCodeBlockSuggestionId: undefined, + source: undefined, }); } } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index c150cdf5bd8..d306002b04d 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -10,6 +10,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { assertDefined } from '../../../../base/common/types.js'; +import product from '../../../../platform/product/common/product.js'; import { registerEditorFeature } from '../../../../editor/common/editorFeatures.js'; import * as nls from '../../../../nls.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; @@ -51,7 +52,7 @@ import { ILanguageModelStatsService, LanguageModelStatsService } from '../common import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { PromptsConfig } from '../common/promptSyntax/config/config.js'; import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, MODE_DEFAULT_SOURCE_FOLDER, MODE_FILE_EXTENSION, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../common/promptSyntax/config/promptFileLocations.js'; -import { registerPromptFileContributions } from '../common/promptSyntax/promptFileContributions.js'; +import { PromptLanguageFeaturesProvider } from '../common/promptSyntax/promptFileContributions.js'; import { INSTRUCTIONS_DOCUMENTATION_URL, MODE_DOCUMENTATION_URL, PROMPT_DOCUMENTATION_URL } from '../common/promptSyntax/promptTypes.js'; import { IPromptsService } from '../common/promptSyntax/service/promptsService.js'; import { PromptsService } from '../common/promptSyntax/service/promptsServiceImpl.js'; @@ -70,6 +71,7 @@ import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js' import { ChatSubmitAction, registerChatExecuteActions } from './actions/chatExecuteActions.js'; import { registerChatFileTreeActions } from './actions/chatFileTreeActions.js'; import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js'; +import { registerChatPromptNavigationActions } from './actions/chatPromptNavigationActions.js'; import { registerChatExportActions } from './actions/chatImportExport.js'; import { registerLanguageModelActions } from './actions/chatLanguageModelActions.js'; import { registerMoveActions } from './actions/chatMoveActions.js'; @@ -306,10 +308,15 @@ configurationRegistry.registerConfiguration({ }, [ChatConfiguration.EmptyStateHistoryEnabled]: { type: 'boolean', - default: false, + default: product.quality === 'insiders', description: nls.localize('chat.emptyState.history.enabled', "Show recent chat history on the empty chat state."), tags: ['experimental'] }, + [ChatConfiguration.NotifyWindowOnResponseReceived]: { + type: 'boolean', + default: false, + description: nls.localize('chat.notifyWindowOnResponseReceived', "Controls whether a chat session should notify the user when a response is received while the window is not in focus. This includes a window badge as well as notification toast."), + }, 'chat.checkpoints.enabled': { type: 'boolean', default: true, @@ -581,6 +588,7 @@ configurationRegistry.registerConfiguration({ }, [PromptsConfig.PROMPT_FILES_SUGGEST_KEY]: { type: 'object', + scope: ConfigurationScope.RESOURCE, title: nls.localize( 'chat.promptFilesRecommendations.title', "Prompt File Recommendations", @@ -638,7 +646,7 @@ configurationRegistry.registerConfiguration({ }, [ChatConfiguration.ThinkingStyle]: { type: 'string', - default: 'fixedScrolling', + default: product.quality === 'insider' ? 'fixedScrolling' : 'collapsed', enum: ['collapsed', 'collapsedPreview', 'expanded', 'none', 'collapsedPerItem', 'fixedScrolling'], enumDescriptions: [ nls.localize('chat.agent.thinkingMode.collapsed', "Thinking parts will be collapsed by default."), @@ -910,6 +918,7 @@ registerWorkbenchContribution2(PromptUrlHandler.ID, PromptUrlHandler, WorkbenchP registerWorkbenchContribution2(ChatSessionsView.ID, ChatSessionsView, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(ChatEditingNotebookFileSystemProviderContrib.ID, ChatEditingNotebookFileSystemProviderContrib, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(UserToolSetsContributions.ID, UserToolSetsContributions, WorkbenchPhase.Eventually); +registerWorkbenchContribution2(PromptLanguageFeaturesProvider.ID, PromptLanguageFeaturesProvider, WorkbenchPhase.Eventually); registerChatActions(); registerChatAccessibilityActions(); @@ -917,6 +926,7 @@ registerChatCopyActions(); registerChatCodeBlockActions(); registerChatCodeCompareBlockActions(); registerChatFileTreeActions(); +registerChatPromptNavigationActions(); registerChatTitleActions(); registerChatExecuteActions(); registerQuickChatActions(); @@ -959,10 +969,6 @@ registerSingleton(IChatTodoListService, ChatTodoListService, InstantiationType.D registerSingleton(IChatOutputRendererService, ChatOutputRendererService, InstantiationType.Delayed); registerSingleton(IChatLayoutService, ChatLayoutService, InstantiationType.Delayed); - -registerPromptFileContributions(); - - registerAction2(ConfigureToolSets); registerAction2(RenameChatSessionAction); registerAction2(DeleteChatSessionAction); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index e1a1b85f8c8..ba20ff26f9c 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -8,24 +8,27 @@ import { Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { Selection } from '../../../../editor/common/core/selection.js'; import { EditDeltaInfo } from '../../../../editor/common/textModelEditSource.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IChatAgentCommand, IChatAgentData } from '../common/chatAgents.js'; import { IChatResponseModel } from '../common/chatModel.js'; import { IParsedChatRequest } from '../common/chatParserTypes.js'; import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js'; -import { IChatElicitationRequest, IChatSendRequestOptions } from '../common/chatService.js'; +import { IChatElicitationRequest, IChatLocationData, IChatSendRequestOptions } from '../common/chatService.js'; import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel } from '../common/chatViewModel.js'; import { ChatAgentLocation, ChatModeKind } from '../common/constants.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; +import { ChatEditorInput } from './chatEditorInput.js'; import { ChatInputPart } from './chatInputPart.js'; import { ChatViewPane } from './chatViewPane.js'; -import { IChatViewState, IChatWidgetContrib } from './chatWidget.js'; +import { ChatWidget, IChatViewState, IChatWidgetContrib } from './chatWidget.js'; import { ICodeBlockActionContext } from './codeBlockPart.js'; export const IChatWidgetService = createDecorator('chatWidgetService'); @@ -47,6 +50,22 @@ export interface IChatWidgetService { getWidgetsByLocations(location: ChatAgentLocation): ReadonlyArray; } +export async function showChatWidgetInViewOrEditor(accessor: ServicesAccessor, widget: IChatWidget) { + if ('viewId' in widget.viewContext) { + await accessor.get(IViewsService).openView(widget.location); + } else { + for (const group of accessor.get(IEditorGroupsService).groups) { + for (const editor of group.editors) { + if (editor instanceof ChatEditorInput && editor.sessionId === widget.viewModel?.sessionId) { + group.openEditor(editor); + return; + } + } + } + } +} + + export async function showChatView(viewsService: IViewsService): Promise { return (await viewsService.openView(ChatViewId))?.widget; } @@ -93,7 +112,7 @@ export const IChatAccessibilityService = createDecorator; setInputPlaceholder(placeholder: string): void; resetInputPlaceholder(): void; - focusLastMessage(): void; + /** + * Focuses the response item in the list. + * @param lastFocused Focuses the most recently focused response. Otherwise, focuses the last response. + */ + focusResponseItem(lastFocused?: boolean): void; focusInput(): void; hasInputFocus(): boolean; getModeRequestOptions(): Partial; @@ -234,6 +259,7 @@ export interface IChatWidget { lockToCodingAgent(name: string, displayName: string, agentId?: string): void; delegateScrollFromMouseWheelEvent(event: IMouseWheelEvent): void; + toggleHistoryVisibility(): void; } diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index e999e1949c3..d70dadfa1d0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { alert, status } from '../../../../base/browser/ui/aria/aria.js'; -import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { AccessibilityProgressSignalScheduler } from '../../../../platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler.js'; @@ -15,37 +15,56 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { AccessibilityVoiceSettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { IChatElicitationRequest } from '../common/chatService.js'; +import { IHostService } from '../../../services/host/browser/host.js'; +import { FocusMode } from '../../../../platform/native/common/native.js'; +import * as dom from '../../../../base/browser/dom.js'; +import { Event } from '../../../../base/common/event.js'; +import { ChatConfiguration } from '../common/constants.js'; +import { localize } from '../../../../nls.js'; +import { ChatWidget } from './chatWidget.js'; const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; export class ChatAccessibilityService extends Disposable implements IChatAccessibilityService { - declare readonly _serviceBrand: undefined; private _pendingSignalMap: DisposableMap = this._register(new DisposableMap()); private _requestId: number = 0; + private readonly notifications: Set = new Set(); + constructor( @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IHostService private readonly _hostService: IHostService ) { super(); } + + override dispose(): void { + for (const ds of Array.from(this.notifications)) { + ds.dispose(); + } + this.notifications.clear(); + super.dispose(); + } + acceptRequest(): number { this._requestId++; this._accessibilitySignalService.playSignal(AccessibilitySignal.chatRequestSent, { allowManyInParallel: true }); this._pendingSignalMap.set(this._requestId, this._instantiationService.createInstance(AccessibilityProgressSignalScheduler, CHAT_RESPONSE_PENDING_ALLOWANCE_MS, undefined)); return this._requestId; } - acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number, isVoiceInput?: boolean): void { + acceptResponse(widget: ChatWidget, container: HTMLElement, response: IChatResponseViewModel | string | undefined, requestId: number, isVoiceInput?: boolean): void { this._pendingSignalMap.deleteAndDispose(requestId); const isPanelChat = typeof response !== 'string'; const responseContent = typeof response === 'string' ? response : response?.response.toString(); this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true }); - if (!response) { + if (!response || !responseContent) { return; } + this._showOSNotification(widget, container, responseContent.substring(0, 20)); const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : ''; const plainTextResponse = renderAsPlaintext(new MarkdownString(responseContent)); if (!isVoiceInput || this._configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) !== 'on') { @@ -58,4 +77,56 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi alert(title + ' ' + message); this._accessibilitySignalService.playSignal(AccessibilitySignal.chatUserActionRequired, { allowManyInParallel: true }); } + + private async _showOSNotification(widget: ChatWidget, container: HTMLElement, responseContent: string): Promise { + if (!this._configurationService.getValue(ChatConfiguration.NotifyWindowOnResponseReceived)) { + return; + } + + const targetWindow = dom.getWindow(container); + if (!targetWindow) { + return; + } + + if (targetWindow.document.hasFocus()) { + return; + } + + await this._hostService.focus(targetWindow, { mode: FocusMode.Notify }); + + // Dispose any previous unhandled notifications to avoid replacement/coalescing. + for (const ds of Array.from(this.notifications)) { + ds.dispose(); + this.notifications.delete(ds); + } + + + const notification = await dom.triggerNotification(localize('chat.responseReceivedNotification', "Chat response received: {0}", responseContent), { + detail: localize('chat.responseReceivedNotification.detail', "Click to focus chat"), + sticky: false, + }); + + if (!notification) { + return; + } + + const disposables = new DisposableStore(); + disposables.add(notification); + this.notifications.add(disposables); + + disposables.add(Event.once(notification.onClick)(async () => { + await this._hostService.focus(targetWindow, { mode: FocusMode.Force }); + widget.input.focus(); + disposables.dispose(); + this.notifications.delete(disposables); + })); + + disposables.add(this._hostService.onDidChangeFocus(focus => { + if (focus) { + disposables.dispose(); + this.notifications.delete(disposables); + } + })); + } + } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAnonymousRateLimitedPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAnonymousRateLimitedPart.ts new file mode 100644 index 00000000000..ad1a40d86c6 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAnonymousRateLimitedPart.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $, append } from '../../../../../base/browser/dom.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { localize } from '../../../../../nls.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; +import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; +import { IChatErrorDetailsPart, IChatRendererContent } from '../../common/chatViewModel.js'; +import { IChatContentPart } from './chatContentParts.js'; + +export class ChatAnonymousRateLimitedPart extends Disposable implements IChatContentPart { + + readonly domNode: HTMLElement; + + constructor( + private readonly content: IChatErrorDetailsPart, + @ICommandService commandService: ICommandService, + @ITelemetryService telemetryService: ITelemetryService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService + ) { + super(); + + this.domNode = $('.chat-rate-limited-widget'); + + const icon = append(this.domNode, $('span')); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.info)); + + const messageContainer = append(this.domNode, $('.chat-rate-limited-message')); + + const message = append(messageContainer, $('div')); + message.textContent = localize('anonymousRateLimited', "Continue the conversation by signing in. Your free account gets 50 premium requests a month plus access to more models and AI features."); + + const signInButton = this._register(new Button(messageContainer, { ...defaultButtonStyles, supportIcons: true })); + signInButton.label = localize('enableMoreAIFeatures', "Enable more AI features"); + signInButton.element.classList.add('chat-rate-limited-button'); + + this._register(signInButton.onDidClick(async () => { + const commandId = 'workbench.action.chat.triggerSetup'; + telemetryService.publicLog2('workbenchActionExecuted', { id: commandId, from: 'chat-response' }); + + await commandService.executeCommand(commandId); + })); + } + + hasSameContent(other: IChatRendererContent): boolean { + return other.kind === this.content.kind && !!other.errorDetails.isRateLimited; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index aefdbc56bf6..131cbf43732 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -16,25 +16,41 @@ import { IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js'; import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../chatAttachmentWidgets.js'; +export interface IChatAttachmentsContentPartOptions { + readonly variables: IChatRequestVariableEntry[]; + readonly contentReferences?: ReadonlyArray; + readonly domNode?: HTMLElement; + readonly limit?: number; +} + export class ChatAttachmentsContentPart extends Disposable { private readonly attachedContextDisposables = this._register(new DisposableStore()); private readonly _onDidChangeVisibility = this._register(new Emitter()); private readonly _contextResourceLabels: ResourceLabels; + private _showingAll = false; + + private readonly variables: IChatRequestVariableEntry[]; + private readonly contentReferences: ReadonlyArray; + private readonly limit?: number; + public readonly domNode: HTMLElement | undefined; public contextMenuHandler?: (attachment: IChatRequestVariableEntry, event: MouseEvent) => void; constructor( - private readonly variables: IChatRequestVariableEntry[], - private readonly contentReferences: ReadonlyArray = [], - public readonly domNode: HTMLElement | undefined = dom.$('.chat-attached-context'), + options: IChatAttachmentsContentPartOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); + this.variables = options.variables; + this.contentReferences = options.contentReferences ?? []; + this.limit = options.limit; + this.domNode = options.domNode ?? dom.$('.chat-attached-context'); + this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); - this.initAttachedContext(domNode); - if (!domNode.childElementCount) { + this.initAttachedContext(this.domNode); + if (!this.domNode.childElementCount) { this.domNode = undefined; } } @@ -44,75 +60,130 @@ export class ChatAttachmentsContentPart extends Disposable { this.attachedContextDisposables.clear(); const hoverDelegate = this.attachedContextDisposables.add(createInstantHoverDelegate()); - for (const attachment of this.variables) { - 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; - const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; - const correspondingContentReference = this.contentReferences.find((ref) => (typeof ref.reference === 'object' && 'variableName' in ref.reference && ref.reference.variableName === attachment.name) || (URI.isUri(ref.reference) && basename(ref.reference.path) === attachment.name)); - const isAttachmentOmitted = correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Omitted; - const isAttachmentPartialOrOmitted = isAttachmentOmitted || correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Partial; + const visibleAttachments = this.getVisibleAttachments(); + const hasMoreAttachments = this.limit && this.variables.length > this.limit && !this._showingAll; - let widget; - if (attachment.kind === 'tool' || attachment.kind === 'toolset') { - widget = this.instantiationService.createInstance(ToolSetOrToolItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isElementVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isImageVariableEntry(attachment)) { - attachment.omittedState = isAttachmentPartialOrOmitted ? OmittedState.Full : attachment.omittedState; - widget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isPromptFileVariableEntry(attachment)) { - if (attachment.automaticallyAdded) { - continue; - } - widget = this.instantiationService.createInstance(PromptFileAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isPromptTextVariableEntry(attachment)) { - if (attachment.automaticallyAdded) { - continue; - } - widget = this.instantiationService.createInstance(PromptTextAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) { - widget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isPasteVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (resource && isNotebookOutputVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isSCMHistoryItemVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isSCMHistoryItemChangeVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(SCMHistoryItemChangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { - widget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } else { - widget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); - } + for (const attachment of visibleAttachments) { + this.renderAttachment(attachment, container, hoverDelegate); + } - let ariaLabel: string | null = null; - - if (isAttachmentPartialOrOmitted) { - widget.element.classList.add('warning'); - } - const description = correspondingContentReference?.options?.status?.description; - if (isAttachmentPartialOrOmitted) { - ariaLabel = `${ariaLabel}${description ? ` ${description}` : ''}`; - for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { - const element = widget.label.element.querySelector(selector); - if (element) { - element.classList.add('warning'); - } - } - } - - this._register(dom.addDisposableListener(widget.element, 'contextmenu', e => this.contextMenuHandler?.(attachment, e))); - - if (this.attachedContextDisposables.isDisposed) { - widget.dispose(); - return; - } - - if (ariaLabel) { - widget.element.ariaLabel = ariaLabel; - } - - this.attachedContextDisposables.add(widget); + if (hasMoreAttachments) { + this.renderShowMoreButton(container); } } + + private getVisibleAttachments(): IChatRequestVariableEntry[] { + if (!this.limit || this._showingAll) { + return this.variables; + } + return this.variables.slice(0, this.limit); + } + + private renderShowMoreButton(container: HTMLElement) { + const remainingCount = this.variables.length - (this.limit ?? 0); + + // Create a button that looks like the attachment pills + const showMoreButton = dom.$('div.chat-attached-context-attachment.chat-attachments-show-more-button'); + showMoreButton.setAttribute('role', 'button'); + showMoreButton.setAttribute('tabindex', '0'); + showMoreButton.style.cursor = 'pointer'; + + // Add pill icon (ellipsis) + const pillIcon = dom.$('div.chat-attached-context-pill', {}, dom.$('span.codicon.codicon-ellipsis')); + + // Add text label + const textLabel = dom.$('span.chat-attached-context-custom-text'); + textLabel.textContent = `${remainingCount} more`; + + showMoreButton.appendChild(pillIcon); + showMoreButton.appendChild(textLabel); + + // Add click and keyboard event handlers + const clickHandler = () => { + this._showingAll = true; + this.initAttachedContext(container); + }; + + this.attachedContextDisposables.add(dom.addDisposableListener(showMoreButton, 'click', clickHandler)); + this.attachedContextDisposables.add(dom.addDisposableListener(showMoreButton, 'keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + clickHandler(); + } + })); + + container.appendChild(showMoreButton); + this.attachedContextDisposables.add({ dispose: () => showMoreButton.remove() }); + } + + private renderAttachment(attachment: IChatRequestVariableEntry, container: HTMLElement, hoverDelegate: any) { + 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; + const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; + const correspondingContentReference = this.contentReferences.find((ref) => (typeof ref.reference === 'object' && 'variableName' in ref.reference && ref.reference.variableName === attachment.name) || (URI.isUri(ref.reference) && basename(ref.reference.path) === attachment.name)); + const isAttachmentOmitted = correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Omitted; + const isAttachmentPartialOrOmitted = isAttachmentOmitted || correspondingContentReference?.options?.status?.kind === ChatResponseReferencePartStatusKind.Partial; + + let widget; + if (attachment.kind === 'tool' || attachment.kind === 'toolset') { + widget = this.instantiationService.createInstance(ToolSetOrToolItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isElementVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isImageVariableEntry(attachment)) { + attachment.omittedState = isAttachmentPartialOrOmitted ? OmittedState.Full : attachment.omittedState; + widget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isPromptFileVariableEntry(attachment)) { + if (attachment.automaticallyAdded) { + return; // Skip automatically added prompt files + } + widget = this.instantiationService.createInstance(PromptFileAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isPromptTextVariableEntry(attachment)) { + if (attachment.automaticallyAdded) { + return; // Skip automatically added prompt text + } + widget = this.instantiationService.createInstance(PromptTextAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) { + widget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isPasteVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (resource && isNotebookOutputVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemChangeVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(SCMHistoryItemChangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { + widget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } else { + widget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, correspondingContentReference, undefined, { shouldFocusClearButton: false, supportsDeletion: false }, container, this._contextResourceLabels, hoverDelegate); + } + + let ariaLabel: string | null = null; + + if (isAttachmentPartialOrOmitted) { + widget.element.classList.add('warning'); + } + const description = correspondingContentReference?.options?.status?.description; + if (isAttachmentPartialOrOmitted) { + ariaLabel = `${ariaLabel}${description ? ` ${description}` : ''}`; + for (const selector of ['.monaco-icon-suffix-container', '.monaco-icon-name-container']) { + const element = widget.label.element.querySelector(selector); + if (element) { + element.classList.add('warning'); + } + } + } + + this._register(dom.addDisposableListener(widget.element, 'contextmenu', e => this.contextMenuHandler?.(attachment, e))); + + if (this.attachedContextDisposables.isDisposed) { + widget.dispose(); + return; + } + + if (ariaLabel) { + widget.element.ariaLabel = ariaLabel; + } + + this.attachedContextDisposables.add(widget); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 1193f9baa8b..8793c5fc2b6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -26,6 +26,7 @@ import { ITextModel } from '../../../../../editor/common/model.js'; import { getIconClasses } from '../../../../../editor/common/services/getIconClasses.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js'; import { localize } from '../../../../../nls.js'; import { getFlatContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; @@ -38,7 +39,6 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js'; -import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js'; import { MarkedKatexSupport } from '../../../markdown/browser/markedKatexSupport.js'; import { IMarkdownVulnerability } from '../../common/annotations.js'; import { IEditSessionEntryDiff } from '../../common/chatEditingService.js'; @@ -127,11 +127,11 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP })]) : []; - // Don't set to 'false' for responses, respect defaults - const markedOpts: MarkdownRendererMarkedOptions = isRequestVM(element) ? { + // Enables github-flavored-markdown + line breaks with single newlines (which matches typical expectations but isn't "proper" in markdown) + const markedOpts: MarkdownRendererMarkedOptions = { gfm: true, breaks: true, - } : {}; + }; const result = this._register(renderer.render(markdown.content, { sanitizerConfig: MarkedKatexSupport.getSanitizerOptions({ @@ -267,6 +267,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP modeId: element.model.request?.modeInfo?.modeId, modelId: element.model.request?.modelId, applyCodeBlockSuggestionId: undefined, + source: undefined, }) }; })); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatPullRequestContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatPullRequestContentPart.ts index 2d8d045c1f7..d3df1823d78 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatPullRequestContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatPullRequestContentPart.ts @@ -16,6 +16,7 @@ import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { addDisposableListener } from '../../../../../base/browser/dom.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; +import { renderMarkdown } from '../../../../../base/browser/markdownRenderer.js'; export class ChatPullRequestContentPart extends Disposable implements IChatContentPart { public readonly domNode: HTMLElement; @@ -41,7 +42,8 @@ export class ChatPullRequestContentPart extends Disposable implements IChatConte const descriptionElement = dom.append(contentContainer, dom.$('.description')); const descriptionWrapper = dom.append(descriptionElement, dom.$('.description-wrapper')); - descriptionWrapper.textContent = this.pullRequestContent.description; + const markdown = this._register(renderMarkdown({ value: this.pullRequestContent.description })); + dom.append(descriptionWrapper, markdown.element); const seeMoreContainer = dom.append(descriptionElement, dom.$('.see-more')); const seeMore: HTMLAnchorElement = dom.append(seeMoreContainer, dom.$('a')); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts index c2d815cf30c..0c43e1b645f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatQuotaExceededPart.ts @@ -26,20 +26,23 @@ import { IChatContentPart } from './chatContentParts.js'; const $ = dom.$; /** - * Once the sign up button is clicked, and the retry button has been shown, it should be shown every time. + * Once the sign up button is clicked, and the retry + * button has been shown, it should be shown every time. */ let shouldShowRetryButton = false; /** - * Once the 'retry' button is clicked, the wait warning should be shown every time. + * Once the 'retry' button is clicked, the wait warning + * should be shown every time. */ let shouldShowWaitWarning = false; export class ChatQuotaExceededPart extends Disposable implements IChatContentPart { - public readonly domNode: HTMLElement; + + readonly domNode: HTMLElement; private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; + readonly onDidChangeHeight = this._onDidChangeHeight.event; constructor( element: IChatResponseViewModel, @@ -60,20 +63,18 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.warning)); const messageContainer = dom.append(this.domNode, $('.chat-quota-error-message')); - const markdownContent = renderer.render(new MarkdownString(errorDetails.message)); + const markdownContent = this._register(renderer.render(new MarkdownString(errorDetails.message))); dom.append(messageContainer, markdownContent.element); - let button1Label = ''; + let primaryButtonLabel: string | undefined; switch (chatEntitlementService.entitlement) { case ChatEntitlement.Pro: case ChatEntitlement.ProPlus: - button1Label = localize('enableAdditionalUsage', "Manage paid premium requests"); + primaryButtonLabel = localize('enableAdditionalUsage', "Manage Paid Premium Requests"); break; case ChatEntitlement.Free: - button1Label = localize('upgradeToCopilotPro', "Upgrade to GitHub Copilot Pro"); + primaryButtonLabel = localize('upgradeToCopilotPro', "Upgrade to GitHub Copilot Pro"); break; - default: - button1Label = ''; } let hasAddedWaitWarning = false; @@ -93,14 +94,16 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar } hasAddedRetryButton = true; - const button2 = this._register(new Button(messageContainer, { + const retryButton = this._register(new Button(messageContainer, { buttonBackground: undefined, buttonForeground: asCssVariable(textLinkForeground) })); - button2.element.classList.add('chat-quota-error-secondary-button'); - button2.label = localize('clickToContinue', "Click to retry."); + retryButton.element.classList.add('chat-quota-error-secondary-button'); + retryButton.label = localize('clickToContinue', "Click to Retry"); + this._onDidChangeHeight.fire(); - this._register(button2.onDidClick(() => { + + this._register(retryButton.onDidClick(() => { const widget = chatWidgetService.getWidgetBySessionId(element.sessionId); if (!widget) { return; @@ -113,11 +116,12 @@ export class ChatQuotaExceededPart extends Disposable implements IChatContentPar })); }; - if (button1Label) { - const button1 = this._register(new Button(messageContainer, { ...defaultButtonStyles, supportIcons: true })); - button1.label = button1Label; - button1.element.classList.add('chat-quota-error-button'); - this._register(button1.onDidClick(async () => { + if (primaryButtonLabel) { + const primaryButton = this._register(new Button(messageContainer, { ...defaultButtonStyles, supportIcons: true })); + primaryButton.label = primaryButtonLabel; + primaryButton.element.classList.add('chat-quota-error-button'); + + this._register(primaryButton.onDidClick(async () => { const commandId = chatEntitlementService.entitlement === ChatEntitlement.Free ? 'workbench.action.chat.upgradePlan' : 'workbench.action.chat.manageOverages'; telemetryService.publicLog2('workbenchActionExecuted', { id: commandId, from: 'chat-response' }); await commandService.executeCommand(commandId); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatThinkingContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatThinkingContentPart.ts index 0ba19b7d39b..a728e8adaf5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatThinkingContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatThinkingContentPart.ts @@ -18,13 +18,17 @@ import { ButtonWithIcon } from '../../../../../base/browser/ui/button/button.js' import { Codicon } from '../../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; +function stripImSepMarkers(text: string): string { + return text.replace(/<\|im_sep\|>(\*{4,})?/g, ''); +} + function extractTextFromPart(content: IChatThinkingPart): string { const raw = Array.isArray(content.value) ? content.value.join('') : (content.value || ''); - return raw.replace(/<\|im_sep\|>\*{4,}/g, '').trim(); + return stripImSepMarkers(raw).trim(); } function extractTitleFromThinkingContent(content: string): string | undefined { - const headerMatch = content.match(/^\*\*([^*]+)\*\*\s*\n\n/); + const headerMatch = content.match(/^\*\*([^*]+)\*\*\s*/); return headerMatch ? headerMatch[1].trim() : undefined; } @@ -47,6 +51,8 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen private fixedContainer: HTMLElement | undefined; private headerButton: ButtonWithIcon | undefined; private caret: HTMLElement | undefined; + private lastExtractedTitle: string | undefined; + private hasMultipleItems: boolean = false; constructor( content: IChatThinkingPart, @@ -68,6 +74,9 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen this.fixedScrollingMode = mode === 'fixedScrolling'; this.currentTitle = extractedTitle; + if (extractedTitle !== this.defaultTitle) { + this.lastExtractedTitle = extractedTitle; + } this.currentThinkingValue = this.parseContent(initialText); if (mode === 'expanded' || mode === 'collapsedPreview' || mode === 'fixedScrolling') { this.setExpanded(true); @@ -99,7 +108,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } private parseContent(content: string): string { - let cleaned = content.replace(/<\|im_sep\|>\*{4,}/g, '').trim(); + let cleaned = stripImSepMarkers(content).trim(); if (this.perItemCollapsedMode) { cleaned = cleaned.replace(/^\*\*[^*]+\*\*\s*\n+(?:\s*\n)*/, '').trim(); } @@ -240,17 +249,47 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen } } - // if title is present now (e.g., arrived mid-stream), update the header label - if (!this.fixedScrollingMode) { - const maybeTitle = extractTitleFromThinkingContent(raw); - if (maybeTitle && maybeTitle !== this.currentTitle) { - this.setTitle(maybeTitle); - this.currentTitle = maybeTitle; - } + const extractedTitle = extractTitleFromThinkingContent(raw); + if (!extractedTitle || extractedTitle === this.currentTitle) { + return; + } + this.lastExtractedTitle = extractedTitle; + + if (this.fixedScrollingMode && this.headerButton) { + const label = localize('chat.thinking.fixed.progress.withHeader', 'Thinking: {0}{1}', this.lastExtractedTitle, (!this.perItemCollapsedMode && this.hasMultipleItems) ? '...' : ''); + this.headerButton.label = label; + } else if (!this.perItemCollapsedMode) { + const label = localize('chat.thinking.progress.withHeader', '{0}{1}', this.lastExtractedTitle, (!this.perItemCollapsedMode && this.hasMultipleItems) ? '...' : ''); + this.setTitle(label); + this.currentTitle = label; + } else { + this.setTitle(this.lastExtractedTitle); + this.currentTitle = this.lastExtractedTitle; } } public finalizeTitleIfDefault(): void { + if (this.fixedScrollingMode) { + let finalLabel: string; + if (this.lastExtractedTitle) { + finalLabel = localize('chat.thinking.fixed.done.withHeader', '{0}{1}', this.lastExtractedTitle, (!this.perItemCollapsedMode && this.hasMultipleItems) ? '...' : ''); + } else { + finalLabel = localize('chat.thinking.fixed.done.generic', 'Thought for a few seconds'); + } + if (this.headerButton) { + this.headerButton.label = finalLabel; + this.headerButton.icon = Codicon.passFilled; + } + + this.currentTitle = finalLabel; + + if (this.fixedContainer) { + this.fixedContainer.classList.toggle('finished', true); + this.setFixedCollapsedState(true); + } + return; + } + if (this.currentTitle === this.defaultTitle) { const suffix = localize('chat.thinking.fixed.done.generic', 'Thought for a few seconds'); this.setTitle(suffix); @@ -274,6 +313,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen // makes a new text container. when we update, we now update this container. public setupThinkingContainer(content: IChatThinkingPart, context: IChatContentPartRenderContext) { + this.hasMultipleItems = true; if (this.perItemCollapsedMode) { this.createThinkingItemContainer(); } else { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTodoListWidget.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTodoListWidget.ts index dac5573b3d9..01043836ca7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTodoListWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatTodoListWidget.ts @@ -136,14 +136,14 @@ export class ChatTodoListWidget extends Disposable { this.updateTodoDisplay(); } - public clear(sessionId: string | undefined): void { + public clear(sessionId: string | undefined, force: boolean = false): void { if (!sessionId || this.domNode.style.display === 'none') { return; } const currentTodos = this.chatTodoListService.getTodos(sessionId); - const todoListCompleted = !currentTodos.some(todo => todo.status !== 'completed'); - if (todoListCompleted) { + const shouldClear = force || !currentTodos.some(todo => todo.status !== 'completed'); + if (shouldClear) { this.clearAllTodos(); } } @@ -360,28 +360,8 @@ export class ChatTodoListWidget extends Disposable { let title = progressText.textContent || ''; if (!this._isExpanded) { let currentTodo: IChatTodo | undefined; - - if (!firstInProgressTodo) { - if (completedCount > 0 && completedCount < totalCount && lastCompletedTodo) { - currentTodo = lastCompletedTodo; - // Add separator - const separator = dom.$('span'); - separator.textContent = ' - '; - titleElement.appendChild(separator); - - const icon = dom.$('.codicon.codicon-check'); - icon.style.color = 'var(--vscode-charts-green)'; - icon.style.marginRight = '4px'; - icon.style.verticalAlign = 'middle'; - titleElement.appendChild(icon); - - // Add completed todo title - const completedText = dom.$('span'); - completedText.textContent = lastCompletedTodo.title; - completedText.style.verticalAlign = 'middle'; - titleElement.appendChild(completedText); - } - } else { + // Priority 1: Show first in-progress todo (matches manageTodoListTool logic) + if (firstInProgressTodo) { currentTodo = firstInProgressTodo; const separator = dom.$('span'); separator.textContent = ' - '; @@ -398,6 +378,25 @@ export class ChatTodoListWidget extends Disposable { inProgressText.style.verticalAlign = 'middle'; titleElement.appendChild(inProgressText); } + // Priority 2: Show last completed todo if not all completed (matches manageTodoListTool logic) + else if (completedCount > 0 && completedCount < totalCount && lastCompletedTodo) { + currentTodo = lastCompletedTodo; + + const separator = dom.$('span'); + separator.textContent = ' - '; + titleElement.appendChild(separator); + + const icon = dom.$('.codicon.codicon-check'); + icon.style.color = 'var(--vscode-charts-green)'; + icon.style.marginRight = '4px'; + icon.style.verticalAlign = 'middle'; + titleElement.appendChild(icon); + + const completedText = dom.$('span'); + completedText.textContent = lastCompletedTodo.title; + completedText.style.verticalAlign = 'middle'; + titleElement.appendChild(completedText); + } if (currentTodo && currentTodo.description && currentTodo.description.trim()) { title = currentTodo.description; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts index 0525980d6f1..b52a35e54ee 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatToolInputOutputContentPart.ts @@ -103,6 +103,7 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @IFileService private readonly _fileService: IFileService, ) { super(); this._currentWidth = width; @@ -220,19 +221,30 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { dom.h('.chat-collapsible-io-resource-actions@actions'), ]); - const entries = parts.map((part): IChatRequestVariableEntry => { + this.fillInResourceGroup(parts, el.items, el.actions).then(() => this._onDidChangeHeight.fire()); + + container.appendChild(el.root); + return el.root; + } + + private async fillInResourceGroup(parts: IChatCollapsibleIODataPart[], itemsContainer: HTMLElement, actionsContainer: HTMLElement) { + const entries = await Promise.all(parts.map(async (part): Promise => { if (part.mimeType && getAttachableImageExtension(part.mimeType)) { - return { kind: 'image', id: generateUuid(), name: basename(part.uri), value: part.value, mimeType: part.mimeType, isURL: false, references: [{ kind: 'reference', reference: part.uri }] }; + const value = part.value ?? await this._fileService.readFile(part.uri).then(f => f.value.buffer, () => undefined); + return { kind: 'image', id: generateUuid(), name: basename(part.uri), value, mimeType: part.mimeType, isURL: false, references: [{ kind: 'reference', reference: part.uri }] }; } else { return { kind: 'file', id: generateUuid(), name: basename(part.uri), fullName: part.uri.path, value: part.uri }; } - }); + })); const attachments = this._register(this._instantiationService.createInstance( ChatAttachmentsContentPart, - entries, - undefined, - undefined, + { + variables: entries, + limit: 5, + contentReferences: undefined, + domNode: undefined + } )); attachments.contextMenuHandler = (attachment, event) => { @@ -251,19 +263,17 @@ export class ChatCollapsibleInputOutputContentPart extends Disposable { } }; - el.items.appendChild(attachments.domNode!); + itemsContainer.appendChild(attachments.domNode!); - const toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, el.actions, MenuId.ChatToolOutputResourceToolbar, { + const toolbar = this._register(this._instantiationService.createInstance(MenuWorkbenchToolBar, actionsContainer, MenuId.ChatToolOutputResourceToolbar, { menuOptions: { shouldForwardArgs: true, }, })); toolbar.context = { parts } satisfies IChatToolOutputResourceToolbarContext; - - container.appendChild(el.root); - return el.root; } + private addCodeBlock(part: IChatCollapsibleIOCodePart, container: HTMLElement) { const data: ICodeBlockData = { languageId: part.languageId, diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css index e0f4a3cc455..97ef0026c73 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatConfirmationWidget.css @@ -43,6 +43,14 @@ } .chat-confirmation-widget-container { + position: relative; + + > .monaco-progress-container.active { + top: 0px; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + } + .chat-collapsible-top-level-resource-group { margin-top: -12px; margin-bottom: 12px; @@ -271,6 +279,7 @@ .monaco-button { overflow-wrap: break-word; padding: 2px 5px; + width: inherit; } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatPullRequestContent.css b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatPullRequestContent.css index df72d9464d9..345cdae7c23 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatPullRequestContent.css +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/media/chatPullRequestContent.css @@ -35,6 +35,10 @@ border-bottom: 1px solid var(--vscode-chat-requestBorder); } + p { + margin: 0px; + } + .description .see-more { display: none; position: absolute; diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts index 451140d4bd2..242739b4ac2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatInputOutputMarkdownProgressPart.ts @@ -16,7 +16,6 @@ import { IModelService } from '../../../../../../editor/common/services/model.js import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ChatResponseResource } from '../../../common/chatModel.js'; import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../common/chatService.js'; -import { isResponseVM } from '../../../common/chatViewModel.js'; import { IToolResultInputOutputDetails } from '../../../common/languageModelToolsService.js'; import { IChatCodeBlockInfo } from '../../chat.js'; import { IChatContentPartRenderContext } from '../chatContentParts.js'; @@ -93,7 +92,6 @@ export class ChatInputOutputMarkdownProgressPart extends BaseChatToolInvocationS processedOutput = [{ type: 'embed', value: output, isText: true }]; } - const requestId = isResponseVM(context.element) ? context.element.requestId : context.element.id; const collapsibleListPart = this._register(instantiationService.createInstance( ChatCollapsibleInputOutputContentPart, message, @@ -125,7 +123,7 @@ export class ChatInputOutputMarkdownProgressPart extends BaseChatToolInvocationS } // Fall back to text if it's not valid base64 - const permalinkUri = ChatResponseResource.createUri(context.element.sessionId, requestId, toolInvocation.toolCallId, i, permalinkBasename); + const permalinkUri = ChatResponseResource.createUri(context.element.sessionId, toolInvocation.toolCallId, i, permalinkBasename); return { kind: 'data', value: decoded || new TextEncoder().encode(o.value), mimeType: o.mimeType, uri: permalinkUri, audience: o.audience }; } }), diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts index 05f63d8166b..1498a87d612 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts @@ -89,6 +89,11 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS ) { super(toolInvocation); + // Tag for sub-agent styling + if (toolInvocation.fromSubAgent) { + context.container.classList.add('from-sub-agent'); + } + if (!toolInvocation.confirmationMessages) { throw new Error('Confirmation messages are missing'); } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index a565e12c3b0..02ad59b19fb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -5,7 +5,7 @@ import { h } from '../../../../../../base/browser/dom.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { isMarkdownString, MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { MarkdownRenderer } from '../../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; @@ -22,9 +22,9 @@ import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; import '../media/chatTerminalToolProgressPart.css'; import { TerminalContribSettingId } from '../../../../terminal/terminalContribExports.js'; import { ConfigurationTarget } from '../../../../../../platform/configuration/common/configuration.js'; -import { matchesScheme, Schemas } from '../../../../../../base/common/network.js'; import type { ICodeBlockRenderOptions } from '../../codeBlockPart.js'; import { ChatConfiguration } from '../../../common/constants.js'; +import { CommandsRegistry } from '../../../../../../platform/commands/common/commands.js'; export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart { public readonly domNode: HTMLElement; @@ -45,7 +45,6 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart codeBlockModelCollection: CodeBlockModelCollection, @IInstantiationService instantiationService: IInstantiationService, @IOpenerService openerService: IOpenerService, - @IPreferencesService preferencesService: IPreferencesService, ) { super(toolInvocation); @@ -72,7 +71,10 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart if (toolInvocation.pastTenseMessage) { pastTenseMessage = `${typeof toolInvocation.pastTenseMessage === 'string' ? toolInvocation.pastTenseMessage : toolInvocation.pastTenseMessage.value}`; } - const markdownContent = new MarkdownString(pastTenseMessage, { supportThemeIcons: true }); + const markdownContent = new MarkdownString(pastTenseMessage, { + supportThemeIcons: true, + isTrusted: isMarkdownString(toolInvocation.pastTenseMessage) ? toolInvocation.pastTenseMessage.isTrusted : false, + }); const chatMarkdownContent: IChatMarkdownContent = { kind: 'markdownContent', content: markdownContent, @@ -86,50 +88,7 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart wordWrap: 'on' } }; - this.markdownPart = this._register(instantiationService.createInstance(ChatMarkdownContentPart, chatMarkdownContent, context, editorPool, false, codeBlockStartIndex, renderer, { - actionHandler: (content) => { - if (matchesScheme(content, Schemas.https)) { - openerService.open(content); - return; - } - const [type, scopeRaw] = content.split('_'); - switch (type) { - case 'settings': { - if (scopeRaw === 'global') { - preferencesService.openSettings({ - query: `@id:${ChatConfiguration.GlobalAutoApprove}` - }); - } else { - const scope = parseInt(scopeRaw); - const target = !isNaN(scope) ? scope as ConfigurationTarget : undefined; - const options: IOpenSettingsOptions = { - jsonEditor: true, - revealSetting: { - key: TerminalContribSettingId.AutoApprove - } - }; - switch (target) { - case ConfigurationTarget.APPLICATION: preferencesService.openApplicationSettings(options); break; - case ConfigurationTarget.USER: - case ConfigurationTarget.USER_LOCAL: preferencesService.openUserSettings(options); break; - case ConfigurationTarget.USER_REMOTE: preferencesService.openRemoteSettings(options); break; - case ConfigurationTarget.WORKSPACE: - case ConfigurationTarget.WORKSPACE_FOLDER: preferencesService.openWorkspaceSettings(options); break; - default: { - // Fallback if something goes wrong - preferencesService.openSettings({ - target: ConfigurationTarget.USER, - query: `@id:${TerminalContribSettingId.AutoApprove}`, - }); - break; - } - } - } - break; - } - } - }, - }, currentWidthDelegate(), codeBlockModelCollection, { codeBlockRenderOptions })); + this.markdownPart = this._register(instantiationService.createInstance(ChatMarkdownContentPart, chatMarkdownContent, context, editorPool, false, codeBlockStartIndex, renderer, {}, currentWidthDelegate(), codeBlockModelCollection, { codeBlockRenderOptions })); this._register(this.markdownPart.onDidChangeHeight(() => this._onDidChangeHeight.fire())); elements.message.append(this.markdownPart.domNode); @@ -137,3 +96,40 @@ export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart this.domNode = progressPart.domNode; } } + +export const openTerminalSettingsLinkCommandId = '_chat.openTerminalSettingsLink'; + +CommandsRegistry.registerCommand(openTerminalSettingsLinkCommandId, async (accessor, scopeRaw: string) => { + const preferencesService = accessor.get(IPreferencesService); + + if (scopeRaw === 'global') { + preferencesService.openSettings({ + query: `@id:${ChatConfiguration.GlobalAutoApprove}` + }); + } else { + const scope = parseInt(scopeRaw); + const target = !isNaN(scope) ? scope as ConfigurationTarget : undefined; + const options: IOpenSettingsOptions = { + jsonEditor: true, + revealSetting: { + key: TerminalContribSettingId.AutoApprove + } + }; + switch (target) { + case ConfigurationTarget.APPLICATION: preferencesService.openApplicationSettings(options); break; + case ConfigurationTarget.USER: + case ConfigurationTarget.USER_LOCAL: preferencesService.openUserSettings(options); break; + case ConfigurationTarget.USER_REMOTE: preferencesService.openRemoteSettings(options); break; + case ConfigurationTarget.WORKSPACE: + case ConfigurationTarget.WORKSPACE_FOLDER: preferencesService.openWorkspaceSettings(options); break; + default: { + // Fallback if something goes wrong + preferencesService.openSettings({ + target: ConfigurationTarget.USER, + query: `@id:${TerminalContribSettingId.AutoApprove}`, + }); + break; + } + } + } +}); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts index 72f7cfc4475..855a0a61326 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts @@ -29,7 +29,7 @@ import { AcceptToolConfirmationActionId } from '../../actions/chatToolActions.js import { IChatCodeBlockInfo, IChatWidgetService } from '../../chat.js'; import { renderFileWidgets } from '../../chatInlineAnchorWidget.js'; import { ICodeBlockRenderOptions } from '../../codeBlockPart.js'; -import { ChatCustomConfirmationWidget, IChatConfirmationButton, ChatConfirmationWidget } from '../chatConfirmationWidget.js'; +import { ChatConfirmationWidget, ChatCustomConfirmationWidget, IChatConfirmationButton } from '../chatConfirmationWidget.js'; import { IChatContentPartRenderContext } from '../chatContentParts.js'; import { IChatMarkdownAnchorService } from '../chatMarkdownAnchorService.js'; import { ChatMarkdownContentPart, EditorPool } from '../chatMarkdownContentPart.js'; @@ -66,6 +66,11 @@ export class ToolConfirmationSubPart extends BaseChatToolInvocationSubPart { ) { super(toolInvocation); + // Tag for sub-agent styling + if (toolInvocation.fromSubAgent) { + context.container.classList.add('from-sub-agent'); + } + if (!toolInvocation.confirmationMessages) { throw new Error('Confirmation messages are missing'); } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts index ff3a14405eb..c206004e12f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts @@ -58,6 +58,9 @@ export class ChatToolInvocationPart extends Disposable implements IChatContentPa super(); this.domNode = dom.$('.chat-tool-invocation-part'); + if (toolInvocation.fromSubAgent) { + this.domNode.classList.add('from-sub-agent'); + } if (toolInvocation.presentation === 'hidden') { return; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index d57383a750f..d9f782fc7e7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -143,7 +143,6 @@ registerAction2(class AcceptAction extends WorkingSetAction { id: 'chatEditing.acceptFile', title: localize2('accept.file', 'Keep'), icon: Codicon.check, - precondition: ChatContextKeys.requestInProgress.negate(), menu: [{ when: ContextKeyExpr.and(ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), ContextKeyExpr.notIn(chatEditingResourceContextKey.key, decidedChatEditingResourceContextKey.key)), id: MenuId.MultiDiffEditorFileToolbar, @@ -169,7 +168,6 @@ registerAction2(class DiscardAction extends WorkingSetAction { id: 'chatEditing.discardFile', title: localize2('discard.file', 'Undo'), icon: Codicon.discard, - precondition: ChatContextKeys.requestInProgress.negate(), menu: [{ when: ContextKeyExpr.and(ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), ContextKeyExpr.notIn(chatEditingResourceContextKey.key, decidedChatEditingResourceContextKey.key)), id: MenuId.MultiDiffEditorFileToolbar, @@ -197,10 +195,10 @@ export class ChatEditingAcceptAllAction extends EditingSessionAction { title: localize('accept', 'Keep'), icon: Codicon.check, tooltip: localize('acceptAllEdits', 'Keep All Edits'), - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: hasUndecidedChatEditingResourceContextKey, keybinding: { primary: KeyMod.CtrlCmd | KeyCode.Enter, - when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput), + when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput), weight: KeybindingWeight.WorkbenchContrib, }, menu: [ @@ -229,7 +227,7 @@ export class ChatEditingDiscardAllAction extends EditingSessionAction { title: localize('discard', 'Undo'), icon: Codicon.discard, tooltip: localize('discardAllEdits', 'Undo All Edits'), - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), + precondition: hasUndecidedChatEditingResourceContextKey, menu: [ { id: MenuId.ChatEditingWidgetToolbar, @@ -239,7 +237,7 @@ export class ChatEditingDiscardAllAction extends EditingSessionAction { } ], keybinding: { - when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput, ChatContextKeys.inputHasText.negate()), + when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput, ChatContextKeys.inputHasText.negate()), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.Backspace, }, diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts index 82655c0d0d2..b17d6c243ed 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts @@ -2,27 +2,27 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize, localize2 } from '../../../../../nls.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { Action2, IAction2Options, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { CHAT_CATEGORY } from '../actions/chatActions.js'; -import { ctxHasEditorModification, ctxHasRequestInProgress, ctxIsGlobalEditingSession, ctxReviewModeEnabled } from './chatEditingEditorContextKeys.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; -import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; -import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, IChatEditingService, IChatEditingSession, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ModifiedFileEntryState } from '../../common/chatEditingService.js'; -import { resolveCommandsContext } from '../../../../browser/parts/editor/editorCommandsContext.js'; -import { IListService } from '../../../../../platform/list/browser/listService.js'; -import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; -import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { Action2, IAction2Options, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { IListService } from '../../../../../platform/list/browser/listService.js'; +import { resolveCommandsContext } from '../../../../browser/parts/editor/editorCommandsContext.js'; import { ActiveEditorContext } from '../../../../common/contextkeys.js'; import { EditorResourceAccessor, SideBySideEditor, TEXT_DIFF_EDITOR_ID } from '../../../../common/editor.js'; -import { ChatContextKeys } from '../../common/chatContextKeys.js'; +import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; +import { ACTIVE_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js'; +import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; import { NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_EDITOR_FOCUSED } from '../../../notebook/common/notebookContextKeys.js'; +import { ChatContextKeys } from '../../common/chatContextKeys.js'; +import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, IChatEditingService, IChatEditingSession, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ModifiedFileEntryState } from '../../common/chatEditingService.js'; +import { CHAT_CATEGORY } from '../actions/chatActions.js'; +import { ctxHasEditorModification, ctxIsCurrentlyBeingModified, ctxIsGlobalEditingSession, ctxReviewModeEnabled } from './chatEditingEditorContextKeys.js'; abstract class ChatEditingEditorAction extends Action2 { @@ -170,7 +170,7 @@ abstract class KeepOrUndoAction extends ChatEditingEditorAction { tooltip: _keep ? localize2('accept3', 'Keep Chat Edits in this File') : localize2('discard3', 'Undo Chat Edits in this File'), - precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxHasRequestInProgress.negate()), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxIsCurrentlyBeingModified.negate()), icon: _keep ? Codicon.check : Codicon.discard, @@ -187,7 +187,7 @@ abstract class KeepOrUndoAction extends ChatEditingEditorAction { group: 'a_resolve', order: _keep ? 0 : 1, when: ContextKeyExpr.or( - ContextKeyExpr.and(ctxIsGlobalEditingSession.negate(), ctxHasRequestInProgress.negate()), // Inline chat + ContextKeyExpr.and(ctxIsGlobalEditingSession.negate(), ctxIsCurrentlyBeingModified.negate()), // Inline chat ContextKeyExpr.and(ctxIsGlobalEditingSession, !_keep ? ctxReviewModeEnabled : undefined), // Panel chat ) } @@ -233,7 +233,7 @@ abstract class AcceptRejectHunkAction extends ChatEditingEditorAction { { id: _accept ? 'chatEditor.action.acceptHunk' : 'chatEditor.action.undoHunk', title: _accept ? localize2('acceptHunk', 'Keep this Change') : localize2('undo', 'Undo this Change'), - precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxHasRequestInProgress.negate()), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxIsCurrentlyBeingModified.negate()), icon: _accept ? Codicon.check : Codicon.discard, f1: true, keybinding: { @@ -308,7 +308,7 @@ class ToggleAccessibleDiffViewAction extends ChatEditingEditorAction { id: 'chatEditor.action.showAccessibleDiffView', title: localize2('accessibleDiff', 'Show Accessible Diff View for Chat Edits'), f1: true, - precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxHasRequestInProgress.negate()), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxIsCurrentlyBeingModified.negate()), keybinding: { when: EditorContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib, @@ -328,12 +328,12 @@ export class ReviewChangesAction extends ChatEditingEditorAction { super({ id: 'chatEditor.action.reviewChanges', title: localize2('review', "Review"), - precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxHasRequestInProgress.negate()), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxIsCurrentlyBeingModified.negate()), menu: [{ id: MenuId.ChatEditingEditorContent, group: 'a_resolve', order: 3, - when: ContextKeyExpr.and(ctxReviewModeEnabled.negate(), ctxHasRequestInProgress.negate()), + when: ContextKeyExpr.and(ctxReviewModeEnabled.negate(), ctxIsCurrentlyBeingModified.negate()), }] }); } @@ -352,7 +352,7 @@ export class AcceptAllEditsAction extends ChatEditingEditorAction { id: AcceptAllEditsAction.ID, title: localize2('acceptAllEdits', 'Keep All Chat Edits'), tooltip: localize2('acceptAllEditsTooltip', 'Keep All Chat Edits in this Session'), - precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxHasRequestInProgress.negate()), + precondition: ContextKeyExpr.and(ctxHasEditorModification, ctxIsCurrentlyBeingModified.negate()), icon: Codicon.checkAll, f1: true, keybinding: { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts index 6e0c2fe0ae9..f003f941d1a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorContextKeys.ts @@ -18,6 +18,7 @@ import { IChatService } from '../../common/chatService.js'; export const ctxIsGlobalEditingSession = new RawContextKey('chatEdits.isGlobalEditingSession', undefined, localize('chat.ctxEditSessionIsGlobal', "The current editor is part of the global edit session")); export const ctxHasEditorModification = new RawContextKey('chatEdits.hasEditorModifications', undefined, localize('chat.hasEditorModifications', "The current editor contains chat modifications")); +export const ctxIsCurrentlyBeingModified = new RawContextKey('chatEdits.isCurrentlyBeingModified', undefined, localize('chat.isCurrentlyBeingModified', "The current editor is currently being modified")); export const ctxReviewModeEnabled = new RawContextKey('chatEdits.isReviewModeEnabled', true, localize('chat.ctxReviewModeEnabled', "Review mode for chat changes is enabled")); export const ctxHasRequestInProgress = new RawContextKey('chatEdits.isRequestInProgress', false, localize('chat.ctxHasRequestInProgress', "The current editor shows a file from an edit session which is still in progress")); export const ctxRequestCount = new RawContextKey('chatEdits.requestCount', 0, localize('chatEdits.requestCount', "The number of turns the editing session in this editor has")); @@ -74,6 +75,7 @@ class ContextKeyGroup { private readonly _ctxHasEditorModification: IContextKey; private readonly _ctxHasRequestInProgress: IContextKey; private readonly _ctxReviewModeEnabled: IContextKey; + private readonly _ctxIsCurrentlyBeingModified: IContextKey; private readonly _ctxRequestCount: IContextKey; private readonly _store = new DisposableStore(); @@ -86,14 +88,13 @@ class ContextKeyGroup { ) { this._ctxIsGlobalEditingSession = ctxIsGlobalEditingSession.bindTo(group.scopedContextKeyService); this._ctxHasEditorModification = ctxHasEditorModification.bindTo(group.scopedContextKeyService); + this._ctxIsCurrentlyBeingModified = ctxIsCurrentlyBeingModified.bindTo(group.scopedContextKeyService); this._ctxHasRequestInProgress = ctxHasRequestInProgress.bindTo(group.scopedContextKeyService); this._ctxReviewModeEnabled = ctxReviewModeEnabled.bindTo(group.scopedContextKeyService); this._ctxRequestCount = ctxRequestCount.bindTo(group.scopedContextKeyService); const editorObs = observableFromEvent(this, group.onDidModelChange, () => group.activeEditor); - - this._store.add(autorun(r => { - + const tupleObs = derived(r => { const editor = editorObs.read(r); const uri = EditorResourceAccessor.getOriginalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }); @@ -102,8 +103,11 @@ class ContextKeyGroup { return; } - const tuple = new ObservableEditorSession(uri, chatEditingService, inlineChatSessionService).value.read(r); + return new ObservableEditorSession(uri, chatEditingService, inlineChatSessionService).value.read(r); + }); + this._store.add(autorun(r => { + const tuple = tupleObs.read(r); if (!tuple) { this._reset(); return; @@ -117,6 +121,7 @@ class ContextKeyGroup { this._ctxIsGlobalEditingSession.set(session.isGlobalEditingSession); this._ctxReviewModeEnabled.set(entry ? entry.reviewMode.read(r) : false); this._ctxHasRequestInProgress.set(chatModel?.requestInProgressObs.read(r) ?? false); + this._ctxIsCurrentlyBeingModified.set(!!entry?.isCurrentlyBeingModifiedBy.read(r)); // number of requests const requestCount = chatModel diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index daebbf892a5..aebc71b8754 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -9,7 +9,7 @@ import { ITransaction, autorun, transaction } from '../../../../../base/common/o import { assertType } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { getCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { TextEdit } from '../../../../../editor/common/languages.js'; +import { Location, TextEdit } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../../editor/common/model.js'; import { SingleModelEditStackElement } from '../../../../../editor/common/model/editStack.js'; @@ -188,6 +188,10 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie }; } + public override hasModificationAt(location: Location): boolean { + return location.uri.toString() === this.modifiedModel.uri.toString() && this._textModelChangeService.hasHunkAt(location.range); + } + async restoreFromSnapshot(snapshot: ISnapshotEntry, restoreToDisk = true) { this._stateObs.set(snapshot.state, undefined); await this._textModelChangeService.resetDocumentValues(snapshot.original, restoreToDisk ? snapshot.current : undefined); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index b23b06910c7..7baec8d81a6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -10,7 +10,7 @@ import { Schemas } from '../../../../../base/common/network.js'; import { clamp } from '../../../../../base/common/numbers.js'; import { autorun, derived, IObservable, ITransaction, observableValue, observableValueOpts, transaction } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; -import { TextEdit } from '../../../../../editor/common/languages.js'; +import { Location, TextEdit } from '../../../../../editor/common/languages.js'; import { localize } from '../../../../../nls.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; @@ -149,11 +149,11 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im if (inProgress === false && !this.reviewMode.read(r)) { // AUTO accept mode (when request is done) - const acceptTimeout = this._autoAcceptTimeout.get() * 1000; + const acceptTimeout = this._autoAcceptTimeout.read(undefined) * 1000; const future = Date.now() + acceptTimeout; const update = () => { - const reviewMode = this.reviewMode.get(); + const reviewMode = this.reviewMode.read(undefined); if (reviewMode) { // switched back to review mode this._autoAcceptCtrl.set(undefined, undefined); @@ -182,6 +182,8 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im } } + public abstract hasModificationAt(location: Location): boolean; + acquire() { this._refCounter++; return this; @@ -261,6 +263,7 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im ), feature: this._telemetryInfo.feature, languageId: action.languageId, + source: undefined, }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts index bc1f20958fd..3dc39736b9f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedNotebookEntry.ts @@ -18,7 +18,7 @@ import { LineRange } from '../../../../../editor/common/core/ranges/lineRange.js import { Range } from '../../../../../editor/common/core/range.js'; import { nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; import { DetailedLineRangeMapping, RangeMapping } from '../../../../../editor/common/diff/rangeMapping.js'; -import { TextEdit } from '../../../../../editor/common/languages.js'; +import { Location, TextEdit } from '../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../editor/common/model.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; @@ -197,6 +197,10 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie this._register(this.modifiedModel.onDidChangeContent(this.mirrorNotebookEdits, this)); } + public override hasModificationAt(location: Location): boolean { + return this.cellEntryMap.get(location.uri)?.hasModificationAt(location.range) ?? false; + } + initializeModelsFromDiffImpl(cellsDiffInfo: CellDiffInfo[]) { this.cellEntryMap.forEach(entry => entry.dispose()); this.cellEntryMap.clear(); @@ -998,7 +1002,7 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie if (this.modifiedModel.cells.indexOf(cell) === -1) { return; } - const diffs = this.cellsDiffInfo.get().slice(); + const diffs = this.cellsDiffInfo.read(undefined).slice(); const index = this.modifiedModel.cells.indexOf(cell); let entry = diffs.find(entry => entry.modifiedCellIndex === index); if (!entry) { @@ -1007,13 +1011,13 @@ export class ChatEditingModifiedNotebookEntry extends AbstractChatEditingModifie } const entryIndex = diffs.indexOf(entry); entry.diff.set(cellEntry.diffInfo.read(r), undefined); - if (cellEntry.diffInfo.get().identical && entry.type === 'modified') { + if (cellEntry.diffInfo.read(undefined).identical && entry.type === 'modified') { entry = { ...entry, type: 'unchanged', }; } - if (!cellEntry.diffInfo.get().identical && entry.type === 'unchanged') { + if (!cellEntry.diffInfo.read(undefined).identical && entry.type === 'unchanged') { entry = { ...entry, type: 'modified', diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 267571f8e44..ed46048b561 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -111,7 +111,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio private readonly _entriesObs = observableValue(this, []); public get entries(): IObservable { - this._assertNotDisposed(); return this._entriesObs; } @@ -319,35 +318,30 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } async accept(...uris: URI[]): Promise { - this._assertNotDisposed(); - - if (uris.length === 0) { - await Promise.all(this._entriesObs.get().map(entry => entry.accept())); + if (await this._operateEntry('accept', uris)) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true }); } - for (const uri of uris) { - const entry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri)); - if (entry) { - await entry.accept(); - } - } - this._accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true }); } async reject(...uris: URI[]): Promise { + if (await this._operateEntry('reject', uris)) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true }); + } + } + + private async _operateEntry(action: 'accept' | 'reject', uris: URI[]): Promise { this._assertNotDisposed(); - if (uris.length === 0) { - await Promise.all(this._entriesObs.get().map(entry => entry.reject())); + const applicableEntries = this._entriesObs.get() + .filter(e => uris.length === 0 || uris.some(u => isEqual(u, e.modifiedURI))) + .filter(e => !e.isCurrentlyBeingModifiedBy.get()); + + for (const entry of applicableEntries) { + await entry[action](); } - for (const uri of uris) { - const entry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri)); - if (entry) { - await entry.reject(); - } - } - this._accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true }); + return applicableEntries.length; } async show(previousChanges?: boolean): Promise { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts index 8fcd12e5419..186d7f0748f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTextModelChangeService.ts @@ -15,7 +15,8 @@ import { assertType } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { EditOperation, ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js'; import { StringEdit } from '../../../../../editor/common/core/edits/stringEdit.js'; -import { Range } from '../../../../../editor/common/core/range.js'; +import { IRange, Range } from '../../../../../editor/common/core/range.js'; +import { LineRange } from '../../../../../editor/common/core/ranges/lineRange.js'; import { IDocumentDiff, nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; import { DetailedLineRangeMapping } from '../../../../../editor/common/diff/rangeMapping.js'; import { TextEdit, VersionedExtensionId } from '../../../../../editor/common/languages.js'; @@ -23,7 +24,7 @@ import { IModelDeltaDecoration, ITextModel, ITextSnapshot, MinimapPosition, Over import { ModelDecorationOptions } from '../../../../../editor/common/model/textModel.js'; import { offsetEditFromContentChanges, offsetEditFromLineRangeMapping, offsetEditToEditOperations } from '../../../../../editor/common/model/textModelStringEdit.js'; import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; -import { TextModelEditSource, EditSources } from '../../../../../editor/common/textModelEditSource.js'; +import { EditSources, TextModelEditSource } from '../../../../../editor/common/textModelEditSource.js'; import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { editorSelectionBackground } from '../../../../../platform/theme/common/colorRegistry.js'; @@ -417,6 +418,11 @@ export class ChatEditingTextModelChangeService extends Disposable { } } + public hasHunkAt(range: IRange) { + // return true if the range overlaps a diff range + return this._diffInfo.get().changes.some(c => c.modified.intersectsStrict(LineRange.fromRangeInclusive(range))); + } + private async _updateDiffInfo(): Promise { if (this.originalModel.isDisposed() || this.modifiedModel.isDisposed() || this._store.isDisposed) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts index 44186fcb9ec..5a61bece089 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookCellEntry.ts @@ -6,6 +6,7 @@ import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { IObservable, observableValue, transaction } from '../../../../../../base/common/observable.js'; import { URI } from '../../../../../../base/common/uri.js'; +import { IRange } from '../../../../../../editor/common/core/range.js'; import { IDocumentDiff } from '../../../../../../editor/common/diff/documentDiffProvider.js'; import { DetailedLineRangeMapping } from '../../../../../../editor/common/diff/rangeMapping.js'; import { TextEdit } from '../../../../../../editor/common/languages.js'; @@ -75,6 +76,10 @@ export class ChatEditingNotebookCellEntry extends Disposable { } + public hasModificationAt(range: IRange): boolean { + return this._textModelChangeService.hasHunkAt(range); + } + public clearCurrentEditLineDecoration() { if (this.modifiedModel.isDisposed()) { return; 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 2b6f1f1c40c..d8486785aaf 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/notebook/chatEditingNotebookEditorIntegration.ts @@ -215,7 +215,7 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I onDidChangeVisibleRanges.read(r); if (!changes.length) { this.cellEditorIntegrations.forEach(({ diff }) => { - diff.set({ ...diff.get(), ...nullDocumentDiff }, undefined); + diff.set({ ...diff.read(undefined), ...nullDocumentDiff }, undefined); }); return; } @@ -258,7 +258,7 @@ class ChatEditingNotebookEditorWidgetIntegration extends Disposable implements I const currentDiff = this.cellEditorIntegrations.get(cell); if (currentDiff) { // Do not unnecessarily trigger a change event - if (!areDocumentDiff2Equal(currentDiff.diff.get(), diff)) { + if (!areDocumentDiff2Equal(currentDiff.diff.read(undefined), diff)) { currentDiff.diff.set(diff, undefined); } } else { diff --git a/src/vs/workbench/contrib/chat/browser/chatElicitationRequestPart.ts b/src/vs/workbench/contrib/chat/browser/chatElicitationRequestPart.ts index 8b77997385b..70581ea7809 100644 --- a/src/vs/workbench/contrib/chat/browser/chatElicitationRequestPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatElicitationRequestPart.ts @@ -6,7 +6,6 @@ import { IAction } from '../../../../base/common/actions.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, observableValue } from '../../../../base/common/observable.js'; import { IChatElicitationRequest } from '../common/chatService.js'; import { ToolDataSource } from '../common/languageModelToolsService.js'; @@ -15,9 +14,7 @@ export class ChatElicitationRequestPart extends Disposable implements IChatElici public state: 'pending' | 'accepted' | 'rejected' = 'pending'; public acceptedResult?: Record; - private readonly _isHiddenValue = observableValue('isHidden', false); - public readonly isHidden: IObservable = this._isHiddenValue; - + private _hideOrDisposeCalled = false; constructor( public readonly title: string | IMarkdownString, public readonly message: string | IMarkdownString, @@ -29,12 +26,24 @@ export class ChatElicitationRequestPart extends Disposable implements IChatElici public readonly reject?: () => Promise, public readonly source?: ToolDataSource, public readonly moreActions?: IAction[], + public readonly onHideOrDispose?: () => void, ) { super(); } hide(): void { - this._isHiddenValue.set(true, undefined, undefined); + if (!this._hideOrDisposeCalled && this.onHideOrDispose) { + this.onHideOrDispose(); + this._hideOrDisposeCalled = true; + } + } + + override dispose(): void { + if (!this._hideOrDisposeCalled && this.onHideOrDispose) { + this.onHideOrDispose(); + this._hideOrDisposeCalled = true; + } + super.dispose(); } public toJSON() { diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 1b9259e7430..cacae1f6818 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -12,7 +12,6 @@ import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser import * as aria from '../../../../base/browser/ui/aria/aria.js'; import { Button, ButtonWithIcon } from '../../../../base/browser/ui/button/button.js'; import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { IAction } from '../../../../base/common/actions.js'; import { DeferredPromise } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; @@ -27,7 +26,6 @@ import { autorun, IObservable, observableValue } from '../../../../base/common/o import { isMacintosh } from '../../../../base/common/platform.js'; import { isEqual } from '../../../../base/common/resources.js'; import { ScrollbarVisibility } from '../../../../base/common/scrollable.js'; -import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js'; import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; @@ -89,7 +87,7 @@ import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, IL import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { PromptsType } from '../common/promptSyntax/promptTypes.js'; import { IPromptsService } from '../common/promptSyntax/service/promptsService.js'; -import { CancelAction, ChatEditingSessionSubmitAction, ChatOpenModelPickerActionId, ChatSubmitAction, IChatExecuteActionContext, OpenModePickerAction } from './actions/chatExecuteActions.js'; +import { CancelAction, ChatDelegateToEditSessionAction, ChatEditingSessionSubmitAction, ChatOpenModelPickerActionId, ChatSubmitAction, IChatExecuteActionContext, OpenModePickerAction } from './actions/chatExecuteActions.js'; import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; import { IChatWidget } from './chat.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; @@ -106,6 +104,8 @@ import { ChatRelatedFiles } from './contrib/chatInputRelatedFilesContrib.js'; import { resizeImage } from './imageUtils.js'; import { IModelPickerDelegate, ModelPickerActionItem } from './modelPicker/modelPickerActionItem.js'; import { IModePickerDelegate, ModePickerActionItem } from './modelPicker/modePickerActionItem.js'; +import { assertType } from '../../../../base/common/types.js'; +import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; const $ = dom.$; @@ -173,11 +173,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge readonly selectedToolsModel: ChatSelectedTools; + public getAttachedContext(sessionId: string) { + const contextArr = new ChatRequestVariableSet(); + contextArr.add(...this.attachmentModel.attachments); + return contextArr; + } + public getAttachedAndImplicitContext(sessionId: string): ChatRequestVariableSet { - const contextArr = new ChatRequestVariableSet(); - - contextArr.add(...this.attachmentModel.attachments); + const contextArr = this.getAttachedContext(sessionId); if ((this.implicitContext?.enabled && this.implicitContext?.value) || (isLocation(this.implicitContext?.value) && this.configurationService.getValue('chat.implicitContext.suggestedContext'))) { const implicitChatVariables = this.implicitContext.toBaseEntries(); @@ -260,6 +264,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private inputActionsToolbar!: MenuWorkbenchToolBar; private addFilesToolbar: MenuWorkbenchToolBar | undefined; + private addFilesButton: AddFilesButton | undefined; get inputEditor() { return this._inputEditor; @@ -280,6 +285,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge */ private promptFileAttached: IContextKey; private chatModeKindKey: IContextKey; + private withinEditSessionKey: IContextKey; + private filePartOfEditSessionKey: IContextKey; private modelWidget: ModelPickerActionItem | undefined; private modeWidget: ModePickerActionItem | undefined; @@ -315,13 +322,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const mode = this._currentModeObservable.get(); const modeId: 'ask' | 'agent' | 'edit' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom'; + const modeInstructions = mode.modeInstructions?.get(); return { kind: this.currentModeKind, isBuiltin: mode.isBuiltin, - instructions: { - content: mode.body?.get(), - toolReferences: mode.variableReferences ? this.toolService.toToolReferences(mode.variableReferences.get()) : undefined - }, + modeInstructions: modeInstructions ? { + content: modeInstructions.content, + toolReferences: this.toolService.toToolReferences(modeInstructions.toolReferences), + metadata: modeInstructions.metadata, + } : undefined, modeId: modeId, applyCodeBlockSuggestionId: undefined, }; @@ -456,6 +465,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.inputEditorHasFocus = ChatContextKeys.inputHasFocus.bindTo(contextKeyService); this.promptFileAttached = ChatContextKeys.hasPromptFile.bindTo(contextKeyService); this.chatModeKindKey = ChatContextKeys.chatModeKind.bindTo(contextKeyService); + this.withinEditSessionKey = ChatContextKeys.withinEditSessionDiff.bindTo(contextKeyService); + this.filePartOfEditSessionKey = ChatContextKeys.filePartOfEditSession.bindTo(contextKeyService); + const chatToolCount = ChatContextKeys.chatToolCount.bindTo(contextKeyService); this._register(autorun(reader => { @@ -516,6 +528,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); } + public setIsWithinEditSession(inInsideDiff: boolean, isFilePartOfEditSession: boolean) { + this.withinEditSessionKey.set(inInsideDiff); + this.filePartOfEditSessionKey.set(isFilePartOfEditSession); + } + private getSelectedModelStorageKey(): string { return `chat.currentLanguageModel.${this.location}`; } @@ -554,6 +571,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.checkModelSupported(); } } + } else { + this.setCurrentLanguageModelToDefault(); } }); } @@ -677,7 +696,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const defaultLanguageModelId = this.languageModelsService.getLanguageModelIds().find(id => this.languageModelsService.lookupLanguageModel(id)?.isDefault); const hasUserSelectableLanguageModels = this.languageModelsService.getLanguageModelIds().find(id => { const model = this.languageModelsService.lookupLanguageModel(id); - return model?.isUserSelectable && !model.isDefault; + return model?.isUserSelectable; }); const defaultModel = hasUserSelectableLanguageModels && defaultLanguageModelId ? { metadata: this.languageModelsService.lookupLanguageModel(defaultLanguageModelId)!, identifier: defaultLanguageModelId } : @@ -767,12 +786,22 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const storageKey = this.getDefaultModeExperimentStorageKey(); const hasSetDefaultMode = this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false); if (!hasSetDefaultMode) { - const defaultModeKey = this.entitlementService.entitlement === ChatEntitlement.Free ? 'chat.defaultModeFree' : 'chat.defaultMode'; - const defaultLanguageModelKey = this.entitlementService.entitlement === ChatEntitlement.Free ? 'chat.defaultLanguageModelFree' : 'chat.defaultLanguageModel'; + const isFree = this.entitlementService.entitlement === ChatEntitlement.Free; + const defaultModeKey = isFree ? 'chat.defaultModeFree' : 'chat.defaultMode'; + const defaultLanguageModelKey = isFree ? 'chat.defaultLanguageModelFree' : 'chat.defaultLanguageModel'; + const isAnonymous = this.entitlementService.anonymous; Promise.all([ this.experimentService.getTreatment(defaultModeKey), this.experimentService.getTreatment(defaultLanguageModelKey), ]).then(([defaultModeTreatment, defaultLanguageModelTreatment]) => { + if (isAnonymous) { + // be deterministic for anonymous users + // to support agentic flows with default + // model. + defaultModeTreatment = ChatModeKind.Agent; + defaultLanguageModelTreatment = undefined; + } + if (typeof defaultModeTreatment === 'string') { this.storageService.store(storageKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); const defaultMode = validateChatMode(defaultModeTreatment); @@ -1238,7 +1267,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu actionViewItemProvider: (action, options) => { if (this.location === ChatAgentLocation.Chat || this.location === ChatAgentLocation.EditorInline) { - if ((action.id === ChatSubmitAction.ID || action.id === CancelAction.ID || action.id === ChatEditingSessionSubmitAction.ID) && action instanceof MenuItemAction) { + if ((action.id === ChatSubmitAction.ID || action.id === CancelAction.ID || action.id === ChatEditingSessionSubmitAction.ID || action.id === ChatDelegateToEditSessionAction.ID) && action instanceof MenuItemAction) { const dropdownAction = this.instantiationService.createInstance(MenuItemAction, { id: 'chat.moreExecuteActions', title: localize('notebook.moreExecuteActionsLabel', "More..."), icon: Codicon.chevronDown }, undefined, undefined, undefined, undefined); return this.instantiationService.createInstance(ChatSubmitDropdownActionItem, action, dropdownAction, { ...options, menuAsChild: false }); } @@ -1322,8 +1351,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge hoverDelegate, actionViewItemProvider: (action, options) => { if (action.id === 'workbench.action.chat.attachContext') { - const viewItem = this.instantiationService.createInstance(AddFilesButton, undefined, action, options); - return viewItem; + const viewItem = this.instantiationService.createInstance(AddFilesButton, this._attachmentModel, action, options); + viewItem.setShowLabel(this._attachmentModel.size === 0 && !this.hasImplicitContextBlock()); + this.addFilesButton = viewItem; + return this.addFilesButton; } return undefined; } @@ -1450,9 +1481,33 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._onDidChangeHeight.fire(); } + this.addFilesButton?.setShowLabel(this._attachmentModel.size === 0 && !this.hasImplicitContextBlock()); + this._indexOfLastOpenedContext = -1; } + private hasImplicitContextBlock(): boolean { + const implicit = this.implicitContext?.value; + if (!implicit) { + return false; + } + const isSuggestedEnabled = this.configurationService.getValue('chat.implicitContext.suggestedContext'); + if (!isSuggestedEnabled) { + return true; + } + + // TODO @justschen: merge this with above showing implicit logic + const isUri = URI.isUri(implicit); + if (isUri || isLocation(implicit)) { + const targetUri = isUri ? implicit : implicit.uri; + const attachments = [...this._attachmentModel.attachments.entries()]; + const currentlyAttached = attachments.some(([, a]) => URI.isUri(a.value) && isEqual(a.value, targetUri)); + const shouldShowImplicit = isUri ? !currentlyAttached : implicit.range; + return !!shouldShowImplicit; + } + return false; + } + private handleAttachmentDeletion(e: KeyboardEvent | unknown, index: number, attachment: IChatRequestVariableEntry) { // Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click) if (dom.isKeyboardEvent(e)) { @@ -1904,6 +1959,7 @@ const chatInputEditorContainerSelector = '.interactive-input-editor'; setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector); class AddFilesButton extends ActionViewItem { + private showLabel: boolean | undefined; constructor(context: unknown, action: IAction, options: IActionViewItemOptions) { super(context, action, { @@ -1914,14 +1970,24 @@ class AddFilesButton extends ActionViewItem { }); } + public setShowLabel(show: boolean): void { + this.showLabel = show; + this.updateLabel(); + } + override render(container: HTMLElement): void { container.classList.add('chat-attachment-button'); super.render(container); + this.updateLabel(); } protected override updateLabel(): void { + if (!this.label) { + return; + } assertType(this.label); - const message = `$(attach) ${this.action.label}`; + this.label.classList.toggle('has-label', this.showLabel); + const message = this.showLabel ? `$(attach) ${this.action.label}` : `$(attach)`; dom.reset(this.label, ...renderLabelWithIcons(message)); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 99e438866b2..522f294d5d8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -89,6 +89,8 @@ import { ChatMarkdownDecorationsRenderer } from './chatMarkdownDecorationsRender import { ChatMarkdownRenderer } from './chatMarkdownRenderer.js'; import { ChatEditorOptions } from './chatOptions.js'; import { ChatCodeBlockContentProvider, CodeBlockPart } from './codeBlockPart.js'; +import { ChatAnonymousRateLimitedPart } from './chatContentParts/chatAnonymousRateLimitedPart.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; const $ = dom.$; @@ -210,6 +212,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('chat.agent.thinkingStyle') === 'collapsedPreview') { this._currentThinkingPart.collapseContent(); } @@ -1277,6 +1282,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer this.updateItemHeight(templateData))); return renderedError; + } else if (content.errorDetails.isRateLimited && this.chatEntitlementService.anonymous) { + const renderedError = this.instantiationService.createInstance(ChatAnonymousRateLimitedPart, content); + return renderedError; } else if (content.errorDetails.confirmationButtons && isLast) { const level = content.errorDetails.level ?? ChatErrorLevel.Error; const errorConfirmation = this.instantiationService.createInstance(ChatErrorConfirmationContentPart, level, new MarkdownString(content.errorDetails.message), content, content.errorDetails.confirmationButtons, this.renderer, context); @@ -1451,7 +1459,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer | undefined, templateData: IChatListItemTemplate) { - return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, undefined); + return this.instantiationService.createInstance(ChatAttachmentsContentPart, { + variables, + contentReferences, + domNode: undefined + }); } private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { @@ -1527,6 +1539,12 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer({ extensionPoint: 'chatOutputRenderers', - activationEventsGenerator: (contributions: IChatOutputRendererContribution[], result) => { + activationEventsGenerator: function* (contributions: readonly IChatOutputRendererContribution[]) { for (const contrib of contributions) { - result.push(`onChatOutputRenderer:${contrib.viewType}`); + yield `onChatOutputRenderer:${contrib.viewType}`; } }, jsonSchema: { diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index 1b23b8cc49c..889c1fcc2ed 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -7,7 +7,7 @@ import { coalesce, isNonEmptyArray } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { Event } from '../../../../base/common/event.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { createCommandUri, MarkdownString } from '../../../../base/common/htmlContent.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js'; import * as strings from '../../../../base/common/strings.js'; @@ -199,9 +199,9 @@ const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.regi } } }, - activationEventsGenerator: (contributions: IRawChatParticipantContribution[], result: { push(item: string): void }) => { + activationEventsGenerator: function* (contributions: readonly IRawChatParticipantContribution[]) { for (const contrib of contributions) { - result.push(`onChatParticipant:${contrib.id}`); + yield `onChatParticipant:${contrib.id}`; } }, }); @@ -354,7 +354,7 @@ export class ChatCompatibilityNotifier extends Disposable implements IWorkbenchC this.registeredWelcomeView = true; const showExtensionLabel = localize('showExtension', "Show Extension"); const mainMessage = localize('chatFailErrorMessage', "Chat failed to load because the installed version of the Copilot Chat extension is not compatible with this version of {0}. Please ensure that the Copilot Chat extension is up to date.", this.productService.nameLong); - const commandButton = `[${showExtensionLabel}](command:${showExtensionsWithIdsCommandId}?${encodeURIComponent(JSON.stringify([[this.productService.defaultChatAgent?.chatExtensionId]]))})`; + const commandButton = `[${showExtensionLabel}](${createCommandUri(showExtensionsWithIdsCommandId, [this.productService.defaultChatAgent?.chatExtensionId])})`; const versionMessage = `Copilot Chat version: ${chatExtension.version}`; const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); this._register(viewsRegistry.registerViewWelcomeContent(ChatViewId, { diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index 2fc9bcb3848..3e1a2be71ff 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -8,15 +8,20 @@ import { Orientation, Sash } from '../../../../base/browser/ui/sash/sash.js'; import { disposableTimeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../base/common/event.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { autorun } from '../../../../base/common/observable.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { Selection } from '../../../../editor/common/core/selection.js'; +import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { localize } from '../../../../nls.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; import { IQuickInputService, IQuickWidget } from '../../../../platform/quickinput/common/quickInput.js'; import { editorBackground, inputBackground, quickInputBackground, quickInputForeground } from '../../../../platform/theme/common/colorRegistry.js'; +import product from '../../../../platform/product/common/product.js'; import { EDITOR_DRAG_AND_DROP_BACKGROUND } from '../../../common/theme.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { ChatModel, isCellTextEditOperation } from '../common/chatModel.js'; @@ -25,6 +30,7 @@ import { IChatProgress, IChatService } from '../common/chatService.js'; import { ChatAgentLocation } from '../common/constants.js'; import { IQuickChatOpenOptions, IQuickChatService, showChatView } from './chat.js'; import { ChatWidget } from './chatWidget.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; export class QuickChatService extends Disposable implements IQuickChatService { readonly _serviceBrand: undefined; @@ -155,6 +161,7 @@ class QuickChat extends Disposable { @IChatService private readonly chatService: IChatService, @ILayoutService private readonly layoutService: ILayoutService, @IViewsService private readonly viewsService: IViewsService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, ) { super(); } @@ -234,9 +241,33 @@ class QuickChat extends Disposable { this.widget.setDynamicChatTreeItemLayout(2, this.maxHeight); this.updateModel(); this.sash = this._register(new Sash(parent, { getHorizontalSashTop: () => parent.offsetHeight }, { orientation: Orientation.HORIZONTAL })); + this.setupDisclaimer(parent); this.registerListeners(parent); } + private setupDisclaimer(parent: HTMLElement): void { + const disclaimerElement = dom.append(parent, dom.$('.disclaimer.hidden')); + const disposables = this._store.add(new DisposableStore()); + + this._register(autorun(reader => { + disposables.clear(); + dom.reset(disclaimerElement); + + const sentiment = this.chatEntitlementService.sentimentObs.read(reader); + const anonymous = this.chatEntitlementService.anonymousObs.read(reader); + const requestInProgress = this.chatService.requestInProgressObs.read(reader); + + const showDisclaimer = !sentiment.installed && anonymous && !requestInProgress; + disclaimerElement.classList.toggle('hidden', !showDisclaimer); + + if (showDisclaimer) { + const markdown = this.instantiationService.createInstance(MarkdownRenderer, {}); + const renderedMarkdown = disposables.add(markdown.render(new MarkdownString(localize({ key: 'termsDisclaimer', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.provider?.default?.name ?? '', product.defaultChatAgent?.termsStatementUrl ?? '', product.defaultChatAgent?.privacyStatementUrl ?? ''), { isTrusted: true }))); + disclaimerElement.appendChild(renderedMarkdown.element); + } + })); + } + private get maxHeight(): number { return this.layoutService.mainContainerDimension.height - QuickChat.DEFAULT_HEIGHT_OFFSET; } diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index 233a1e27e49..bf04fbcdf75 100644 --- a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -29,7 +29,7 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplementation } const chatInputFocused = widget.hasInputFocus(); if (chatInputFocused) { - widget.focusLastMessage(); + widget.focusResponseItem(); } const verifiedWidget: IChatWidget = widget; @@ -38,7 +38,7 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplementation return; } - return new ChatResponseAccessibleProvider(verifiedWidget, focusedItem, chatInputFocused); + return new ChatResponseAccessibleProvider(verifiedWidget, focusedItem); } } @@ -46,8 +46,7 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi private _focusedItem: ChatTreeItem; constructor( private readonly _widget: IChatWidget, - item: ChatTreeItem, - private readonly _chatInputFocused: boolean + item: ChatTreeItem ) { super(); this._focusedItem = item; @@ -126,11 +125,7 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi onClose(): void { this._widget.reveal(this._focusedItem); - if (this._chatInputFocused) { - this._widget.focusInput(); - } else { - this._widget.focus(this._focusedItem); - } + this._widget.focus(this._focusedItem); } provideNextContent(): string | undefined { diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts index b953311989a..781806d43bb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts @@ -6,34 +6,29 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { truncate } from '../../../../base/common/strings.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IEditableData } from '../../../common/views.js'; -import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js'; -import { IChatWidgetService } from '../browser/chat.js'; import { ChatEditorInput } from '../browser/chatEditorInput.js'; -import { IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../common/chatAgents.js'; +import { IChatAgentData, IChatAgentRequest, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { IChatProgress, IChatService } from '../common/chatService.js'; import { ChatSession, ChatSessionStatus, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionsExtensionPoint, IChatSessionsService } from '../common/chatSessionsService.js'; import { ChatSessionUri } from '../common/chatUri.js'; import { ChatAgentLocation, ChatModeKind } from '../common/constants.js'; import { CHAT_CATEGORY } from './actions/chatActions.js'; import { IChatEditorOptions } from './chatEditor.js'; +import { NEW_CHAT_SESSION_ACTION_ID } from './chatSessions/common.js'; import { VIEWLET_ID } from './chatSessions/view/chatSessionsView.js'; -const CODING_AGENT_DOCS = 'https://code.visualstudio.com/docs/copilot/copilot-coding-agent'; - const extensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatSessions', jsonSchema: { @@ -80,9 +75,9 @@ const extensionPoint = ExtensionsRegistry.registerExtensionPoint { + activationEventsGenerator: function* (contribs) { for (const contrib of contribs) { - results.push(`onChatSession:${contrib.type}`); + yield `onChatSession:${contrib.type}`; } } }); @@ -128,7 +123,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ constructor( @ILogService private readonly _logService: ILogService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @IExtensionService private readonly _extensionService: IExtensionService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @@ -240,9 +234,13 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ private _registerMenuItems(contribution: IChatSessionsExtensionPoint): IDisposable { return MenuRegistry.appendMenuItem(MenuId.ViewTitle, { command: { - id: `workbench.action.chat.openNewSessionEditor.${contribution.type}`, + id: `${NEW_CHAT_SESSION_ACTION_ID}.${contribution.type}`, title: localize('interactiveSession.openNewSessionEditor', "New {0} Chat Editor", contribution.displayName), icon: Codicon.plus, + source: { + id: contribution.extensionDescription.identifier.value, + title: contribution.extensionDescription.displayName || contribution.extensionDescription.name, + } }, group: 'navigation', order: 1, @@ -276,8 +274,9 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ override: ChatEditorInput.EditorID, pinned: true, }; + const untitledId = `untitled-${generateUuid()}`; await editorService.openEditor({ - resource: ChatEditorInput.getNewEditorUri().with({ query: `chatSessionType=${type}` }), + resource: ChatSessionUri.forSession(type, untitledId), options, }); } catch (e) { @@ -323,7 +322,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ const disposableStore = new DisposableStore(); this._disposableStores.set(contribution.type, disposableStore); - disposableStore.add(this._registerDynamicAgent(contribution)); + disposableStore.add(this._registerAgent(contribution)); disposableStore.add(this._registerCommands(contribution)); disposableStore.add(this._registerMenuItems(contribution)); } @@ -349,7 +348,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } } - private _registerDynamicAgent(contribution: IChatSessionsExtensionPoint): IDisposable { + private _registerAgent(contribution: IChatSessionsExtensionPoint): IDisposable { const { type: id, name, displayName, description, extensionDescription } = contribution; const { identifier: extensionId, name: extensionName, displayName: extensionDisplayName, publisher: extensionPublisherId } = extensionDescription; const agentData: IChatAgentData = { @@ -362,7 +361,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ isDynamic: true, slashCommands: [], locations: [ChatAgentLocation.Chat], - modes: [ChatModeKind.Agent, ChatModeKind.Ask], // TODO: These are no longer respected + modes: [ChatModeKind.Agent, ChatModeKind.Ask], disambiguation: [], metadata: { themeIcon: Codicon.sendToRemoteAgent, @@ -375,9 +374,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ extensionPublisherId, }; - const agentImpl = this._instantiationService.createInstance(CodingAgentChatImplementation, contribution); - const disposable = this._chatAgentService.registerDynamicAgent(agentData, agentImpl); - return disposable; + return this._chatAgentService.registerAgent(id, agentData); } getAllChatSessionContributions(): IChatSessionsExtensionPoint[] { @@ -394,7 +391,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ }); } - async canResolveItemProvider(chatViewType: string) { + async canResolveItemProvider(chatViewType: string): Promise { await this._extensionService.whenInstalledExtensionsRegistered(); const contribution = this._contributions.get(chatViewType); if (contribution && !this._isContributionAvailable(contribution)) { @@ -428,7 +425,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ public async provideChatSessionItems(chatSessionType: string, token: CancellationToken): Promise { if (!(await this.canResolveItemProvider(chatSessionType))) { - throw Error(`Can not find provider for ${chatSessionType}`); + return []; } const provider = this._itemsProviders.get(chatSessionType); @@ -499,8 +496,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ */ public async provideNewChatSessionItem(chatSessionType: string, options: { request: IChatAgentRequest; - prompt?: string; - history?: any[]; metadata?: any; }, token: CancellationToken): Promise { if (!(await this.canResolveItemProvider(chatSessionType))) { @@ -571,118 +566,3 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ registerSingleton(IChatSessionsService, ChatSessionsService, InstantiationType.Delayed); -/** - * Implementation for individual remote coding agent chat functionality - */ -class CodingAgentChatImplementation extends Disposable implements IChatAgentImplementation { - - constructor( - private readonly chatSession: IChatSessionsExtensionPoint, - @IChatService private readonly chatService: IChatService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, - @IChatSessionsService private readonly chatSessionService: IChatSessionsService, - @IEditorService private readonly editorService: IEditorService, - @ILogService private readonly logService: ILogService, - ) { - super(); - } - - async invoke(request: IChatAgentRequest, progress: (progress: IChatProgress[]) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - const widget = this.chatWidgetService.getWidgetBySessionId(request.sessionId); - - if (!widget) { - return {}; - } - - let chatSession: ChatSession | undefined; - - // Find the first editor that matches the chat session - for (const group of this.editorGroupService.groups) { - if (chatSession) { - break; - } - - for (const editor of group.editors) { - if (editor instanceof ChatEditorInput) { - try { - const chatModel = await this.chatService.loadSessionForResource(editor.resource, request.location, CancellationToken.None); - if (chatModel?.sessionId === request.sessionId) { - // this is the model - const identifier = ChatSessionUri.parse(editor.resource); - - if (identifier) { - chatSession = await this.chatSessionService.provideChatSessionContent(this.chatSession.type, identifier.sessionId, token); - } - break; - } - } catch (error) { - // might not be us - } - } - } - } - - if (chatSession?.requestHandler) { - await chatSession.requestHandler(request, progress, history, token); // TODO: Revisit this function's signature in relation to its extension API (eg: 'history' is not strongly typed here) - } else { - try { - const chatSessionItem = await this.chatSessionService.provideNewChatSessionItem( - this.chatSession.type, - { - request, - prompt: request.message, - history, - }, - token, - ); - const options: IChatEditorOptions = { - pinned: true, - preferredTitle: truncate(chatSessionItem.label, 30), - }; - - // Prefetch the chat session content to make the subsequent editor swap quick - await this.chatSessionService.provideChatSessionContent( - this.chatSession.type, - chatSessionItem.id, - token, - ); - - const activeGroup = this.editorGroupService.activeGroup; - const currentEditor = activeGroup?.activeEditor; - if (currentEditor instanceof ChatEditorInput) { - await this.editorService.replaceEditors([{ - editor: currentEditor, - replacement: { - resource: ChatSessionUri.forSession(this.chatSession.type, chatSessionItem.id), - options, - } - }], activeGroup); - } else { - // Fallback: open in new editor if we couldn't find the current one - await this.editorService.openEditor({ - resource: ChatSessionUri.forSession(this.chatSession.type, chatSessionItem.id), - options, - }); - progress([{ - kind: 'markdownContent', - content: new MarkdownString(localize('continueInNewChat', 'Continue **{0}** in a new chat editor', truncate(chatSessionItem.label, 30))), - }]); - } - } catch (error) { - // NOTE: May end up here if extension does not support 'provideNewChatSessionItem' or that API usage throws - this.logService.error(`Failed to create new chat session for type '${this.chatSession.type}'`, error); - const content = - this.chatSession.type === 'copilot-swe-agent' // TODO: Use contributed error messages - ? new MarkdownString(localize('chatSessionNotFoundCopilot', "Failed to create chat session. Use `#copilotCodingAgent` to begin a new [coding agent session]({0}).", CODING_AGENT_DOCS)) - : new MarkdownString(localize('chatSessionNotFoundGeneric', "Failed to create chat session. Please try again later.")); - progress([{ - kind: 'markdownContent', - content, - }]); - } - } - - return {}; - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionTracker.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionTracker.ts index 0d83f407ccc..23f298d790e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionTracker.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessionTracker.ts @@ -14,6 +14,7 @@ import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider } from '. import { IChatService } from '../../common/chatService.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { IChatModel } from '../../common/chatModel.js'; +import { ChatSessionUri } from '../../common/chatUri.js'; export class ChatSessionTracker extends Disposable { private readonly _onDidChangeEditors = this._register(new Emitter<{ sessionType: string; kind: GroupModelChangeKind }>()); @@ -98,8 +99,9 @@ export class ChatSessionTracker extends Disposable { } } + const parsed = ChatSessionUri.parse(editor.resource); const hybridSession: ChatSessionItemWithProvider = { - id: `${provider.chatSessionType}-local-${index}`, + id: parsed?.sessionId || editor.sessionId || `${provider.chatSessionType}-local-${index}`, label: editor.getName(), iconPath: Codicon.chatSparkle, status, diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts index bdd96c9f76d..914ea2180ea 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts @@ -15,6 +15,9 @@ import { ChatSessionUri } from '../../common/chatUri.js'; import { IChatWidgetService } from '../chat.js'; import { ChatEditorInput } from '../chatEditorInput.js'; + +export const NEW_CHAT_SESSION_ACTION_ID = 'workbench.action.chat.openNewSessionEditor'; + export type ChatSessionItemWithProvider = IChatSessionItem & { readonly provider: IChatSessionItemProvider; isHistory?: boolean; diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts index 3a75ea8ecdb..f209316b91e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts @@ -47,6 +47,7 @@ import { LocalChatSessionsProvider } from '../localChatSessionsProvider.js'; import { IListRenderer, IListVirtualDelegate } from '../../../../../../base/browser/ui/list/list.js'; import { ChatSessionTracker } from '../chatSessionTracker.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { getLocalHistoryDateFormatter } from '../../../../localHistory/browser/localHistory.js'; interface ISessionTemplateData { readonly container: HTMLElement; @@ -336,6 +337,16 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer s.id)); + hybridSessions.forEach(session => { + if (!existingIds.has(session.id)) { + itemsWithProvider.push(session as ChatSessionItemWithProvider); + existingIds.add(session.id); + } + }); processSessionsWithTimeGrouping(itemsWithProvider); } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts index 87e624fa5e0..9de57f260fd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsViewPane.ts @@ -5,7 +5,10 @@ import * as DOM from '../../../../../../base/browser/dom.js'; import { append, $ } from '../../../../../../base/browser/dom.js'; +import { IActionViewItem } from '../../../../../../base/browser/ui/actionbar/actionbar.js'; +import { IBaseActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; import { ITreeContextMenuEvent } from '../../../../../../base/browser/ui/tree/tree.js'; +import { Action, IAction } from '../../../../../../base/common/actions.js'; import { coalesce } from '../../../../../../base/common/arrays.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { FuzzyScore } from '../../../../../../base/common/filters.js'; @@ -13,8 +16,9 @@ import { MarshalledId } from '../../../../../../base/common/marshallingIds.js'; import { truncate } from '../../../../../../base/common/strings.js'; import { URI } from '../../../../../../base/common/uri.js'; import * as nls from '../../../../../../nls.js'; +import { DropdownWithPrimaryActionViewItem } from '../../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; import { getActionBarActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { IMenuService, MenuId } from '../../../../../../platform/actions/common/actions.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 { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; @@ -43,7 +47,7 @@ import { IChatEditorOptions } from '../../chatEditor.js'; import { ChatEditorInput } from '../../chatEditorInput.js'; import { ChatViewPane } from '../../chatViewPane.js'; import { ChatSessionTracker } from '../chatSessionTracker.js'; -import { ChatSessionItemWithProvider, findExistingChatEditorByUri, isLocalChatSessionItem, getSessionItemContextOverlay } from '../common.js'; +import { ChatSessionItemWithProvider, findExistingChatEditorByUri, isLocalChatSessionItem, getSessionItemContextOverlay, NEW_CHAT_SESSION_ACTION_ID } from '../common.js'; import { LocalChatSessionsProvider } from '../localChatSessionsProvider.js'; import { GettingStartedDelegate, GettingStartedRenderer, IGettingStartedItem, SessionsDataSource, SessionsDelegate, SessionsRenderer } from './sessionsTreeRenderer.js'; @@ -121,6 +125,65 @@ export class SessionsViewPane extends ViewPane { return this._isEmpty; } + public override createActionViewItem(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined { + if (action.id.startsWith(NEW_CHAT_SESSION_ACTION_ID)) { + return this.getChatSessionDropdown(action, options); + } + return super.createActionViewItem(action, options); + } + + private getChatSessionDropdown(defaultAction: IAction, options: IBaseActionViewItemOptions) { + const primaryAction = this.instantiationService.createInstance(MenuItemAction, { + id: defaultAction.id, + title: defaultAction.label, + icon: Codicon.plus, + }, undefined, undefined, undefined, undefined); + + const menu = this.menuService.createMenu(MenuId.ChatSessionsMenu, this.contextKeyService); + + const actions = menu.getActions({ shouldForwardArgs: true }); + const primaryActions = getActionBarActions( + actions, + 'submenu', + ).primary.filter(action => { + if (action instanceof MenuItemAction && defaultAction instanceof MenuItemAction) { + if (!action.item.source?.id || !defaultAction.item.source?.id) { + return false; + } + if (action.item.source.id === defaultAction.item.source.id) { + return true; + } + } + return false; + }); + + if (!primaryActions || primaryActions.length === 0) { + return; + } + + const dropdownAction = new Action( + 'selectNewChatSessionOption', + nls.localize('chatSession.selectOption', 'More...'), + 'codicon-chevron-down', + true + ); + + const dropdownActions: IAction[] = []; + + primaryActions.forEach(element => { + dropdownActions.push(element); + }); + + return this.instantiationService.createInstance( + DropdownWithPrimaryActionViewItem, + primaryAction, + dropdownAction, + dropdownActions, + '', + options + ); + } + public refreshTree(): void { if (this.tree && this.isBodyVisible()) { this.refreshTreeWithProgress(); @@ -213,6 +276,8 @@ export class SessionsViewPane extends ViewPane { protected override renderBody(container: HTMLElement): void { super.renderBody(container); + container.classList.add('chat-sessions-view'); + // For Getting Started view (null provider), show simple list if (this.provider === null) { this.renderGettingStartedList(container); @@ -282,6 +347,7 @@ export class SessionsViewPane extends ViewPane { overrideStyles: { listBackground: undefined }, + paddingBottom: SessionsDelegate.ITEM_HEIGHT, setRowLineHeight: false } @@ -304,6 +370,17 @@ export class SessionsViewPane extends ViewPane { } })); + this._register(this.tree.onMouseDblClick(e => { + const scrollingByPage = this.configurationService.getValue('workbench.list.scrollByPage'); + if (e.element === null && !scrollingByPage) { + if (this.provider?.chatSessionType && this.provider.chatSessionType !== 'local') { + this.commandService.executeCommand(`workbench.action.chat.openNewSessionEditor.${this.provider?.chatSessionType}`); + } else { + this.commandService.executeCommand('workbench.action.openChat'); + } + } + })); + // Handle visibility changes to load data this._register(this.onDidChangeBodyVisibility(async visible => { if (visible && this.tree) { diff --git a/src/vs/workbench/contrib/chat/browser/chatSetup.ts b/src/vs/workbench/contrib/chat/browser/chatSetup.ts index ab512a6c898..c576fa92b60 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSetup.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSetup.ts @@ -206,8 +206,6 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { private static readonly SETUP_NEEDED_MESSAGE = new MarkdownString(localize('settingUpCopilotNeeded', "You need to set up GitHub Copilot and be signed in to use Chat.")); private static readonly TRUST_NEEDED_MESSAGE = new MarkdownString(localize('trustNeeded', "You need to trust this workspace to use Chat.")); - private static readonly CHAT_ALLOW_ANONYMOUS_CONFIGURATION_KEY = 'chat.allowAnonymousAccess'; - private readonly _onUnresolvableError = this._register(new Emitter()); readonly onUnresolvableError = this._onUnresolvableError.event; @@ -222,7 +220,8 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, ) { super(); } @@ -293,7 +292,6 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { 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 modeInfo = widget?.input.currentModeInfo; - const languageModel = widget?.input.currentLanguageModel; // We need a signal to know when we can resend the request to // Copilot. Waiting for the registration of the agent is not @@ -353,8 +351,7 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { await chatService.resendRequest(requestModel, { ...widget?.getModeRequestOptions(), - modeInfo, - userSelectedModelId: languageModel, + modeInfo }); } @@ -443,8 +440,8 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { let result: IChatSetupResult | undefined = undefined; try { result = await ChatSetup.getInstance(this.instantiationService, this.context, this.controller).run({ - disableChatViewReveal: true, // we are already in a chat context - forceAnonymous: this.shouldForceAnonymous() // gate anonymous access behind some conditions + disableChatViewReveal: true, // we are already in a chat context + forceAnonymous: this.chatEntitlementService.anonymous // only enable anonymous selectively }); } catch (error) { this.logService.error(`[chat setup] Error during setup: ${toErrorMessage(error)}`); @@ -482,26 +479,6 @@ class SetupAgent extends Disposable implements IChatAgentImplementation { return {}; } - private shouldForceAnonymous(): boolean { - if (this.configurationService.getValue(SetupAgent.CHAT_ALLOW_ANONYMOUS_CONFIGURATION_KEY) !== true) { - return false; // only enabled behind an experimental setting - } - - if (this.context.state.entitlement !== ChatEntitlement.Unknown) { - return false; // only consider signed out users - } - - if (ChatEntitlementRequests.providerId(this.configurationService) === defaultChat.provider.enterprise.id) { - return false; // disable for enterprise users - } - - if (this.location !== ChatAgentLocation.Chat) { - return false; // currently only supported from Chat (TODO@bpasero expand this to more locations) - } - - return true; - } - private replaceAgentInRequestModel(requestModel: IChatRequestModel, chatAgentService: IChatAgentService): IChatRequestModel { const agentPart = requestModel.message.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); if (!agentPart) { @@ -824,6 +801,10 @@ class ChatSetup { } private getDialogTitle(options?: { forceSignInDialog?: boolean }): string { + if (this.chatEntitlementService.anonymous) { + return localize('enableMore', "Enable more AI features"); + } + if (this.context.state.entitlement === ChatEntitlement.Unknown || options?.forceSignInDialog) { return localize('signIn', "Sign in to use GitHub Copilot"); } @@ -858,7 +839,8 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionService private readonly extensionService: IExtensionService, - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); @@ -877,6 +859,10 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } private registerSetupAgents(context: ChatEntitlementContext, controller: Lazy): void { + if (this.configurationService.getValue('chat.experimental.disableCoreAgents')) { + return; // TODO@bpasero eventually remove this when we figured out extension activation issues + } + const defaultAgentDisposables = markAsSingleton(new MutableDisposable()); // prevents flicker on window reload const vscodeAgentDisposables = markAsSingleton(new MutableDisposable()); @@ -922,7 +908,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr vscodeAgentDisposables.clear(); } - if ((context.state.installed && context.state.entitlement !== ChatEntitlement.Unknown && context.state.entitlement !== ChatEntitlement.Unresolved) && !context.state.disabled) { + if (context.state.installed && !context.state.disabled) { vscodeAgentDisposables.clear(); // we need to do this to prevent showing duplicate agent/tool entries in the list } }; @@ -952,7 +938,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr }); } - override async run(accessor: ServicesAccessor, mode?: ChatModeKind, options?: { forceSignInDialog?: boolean; forceNoDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: boolean }): Promise { + override async run(accessor: ServicesAccessor, mode?: ChatModeKind, options?: { forceSignInDialog?: boolean; additionalScopes?: readonly string[]; forceAnonymous?: boolean }): Promise { const viewsService = accessor.get(IViewsService); const layoutService = accessor.get(IWorkbenchLayoutService); const instantiationService = accessor.get(IInstantiationService); @@ -969,14 +955,6 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr chatWidget?.input.setChatMode(mode); } - if (options?.forceNoDialog) { - const chatWidget = await showCopilotView(viewsService, layoutService); - ChatSetup.getInstance(instantiationService, context, controller).skipDialog(); - chatWidget?.acceptInput(localize('setupChat', "Set up chat.")); - - return true; - } - const setup = ChatSetup.getInstance(instantiationService, context, controller); const { success } = await setup.run(options); if (success === false && !lifecycleService.willShutdown) { @@ -1014,11 +992,11 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr } } - class ChatSetupTriggerWithoutDialogAction extends Action2 { + class ChatSetupTriggerAnonymouslyAction extends Action2 { constructor() { super({ - id: 'workbench.action.chat.triggerSetupWithoutDialog', + id: 'workbench.action.chat.triggerSetupAnonymously', title: ChatSetupTriggerAction.CHAT_SETUP_ACTION_LABEL }); } @@ -1029,7 +1007,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr telemetryService.publicLog2('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'api' }); - return commandService.executeCommand(CHAT_SETUP_ACTION_ID, undefined, { forceNoDialog: true }); + return commandService.executeCommand(CHAT_SETUP_ACTION_ID, undefined, { forceAnonymous: true }); } } @@ -1159,7 +1137,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr registerAction2(ChatSetupTriggerAction); registerAction2(ChatSetupTriggerForceSignInDialogAction); registerAction2(ChatSetupFromAccountsAction); - registerAction2(ChatSetupTriggerWithoutDialogAction); + registerAction2(ChatSetupTriggerAnonymouslyAction); registerAction2(UpgradePlanAction); registerAction2(EnableOveragesAction); } diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index ffd7c78f2e4..b8e274caf1d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -44,6 +44,8 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { URI } from '../../../../base/common/uri.js'; import { IInlineCompletionsService } from '../../../../editor/browser/services/inlineCompletionsService.js'; import { IChatSessionsService } from '../common/chatSessionsService.js'; +import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; const gaugeForeground = registerColor('gauge.foreground', { dark: inputValidationInfoBorder, @@ -102,6 +104,9 @@ const defaultChat = { nextEditSuggestionsSetting: product.defaultChatAgent?.nextEditSuggestionsSetting ?? '', manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '', manageOverageUrl: product.defaultChatAgent?.manageOverageUrl ?? '', + provider: product.defaultChatAgent?.provider ?? { default: { id: '', name: '' }, enterprise: { id: '', name: '' }, apple: { id: '', name: '' }, google: { id: '', name: '' } }, + termsStatementUrl: product.defaultChatAgent?.termsStatementUrl ?? '', + privacyStatementUrl: product.defaultChatAgent?.privacyStatementUrl ?? '' }; export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribution { @@ -179,10 +184,6 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu let ariaLabel = localize('chatStatus', "Copilot Status"); let kind: StatusbarEntryKind | undefined; - // Check if there are any chat sessions in progress - const inProgress = this.chatSessionsService.getInProgress(); - const hasInProgressSessions = inProgress.some(item => item.count > 0); - if (isNewUser(this.chatEntitlementService)) { const entitlement = this.chatEntitlementService.entitlement; @@ -202,18 +203,29 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu } else { const chatQuotaExceeded = this.chatEntitlementService.quotas.chat?.percentRemaining === 0; const completionsQuotaExceeded = this.chatEntitlementService.quotas.completions?.percentRemaining === 0; + const chatSessionsInProgressCount = this.chatSessionsService.getInProgress().reduce((total, item) => total + item.count, 0); // Disabled if (this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted) { - text = `$(copilot-unavailable)`; - ariaLabel = localize('copilotDisabledStatus', "Copilot Disabled"); + text = '$(copilot-unavailable)'; + ariaLabel = localize('copilotDisabledStatus', "Copilot disabled"); + } + + // Sessions in progress + else if (chatSessionsInProgressCount > 0) { + text = '$(copilot-in-progress)'; + if (chatSessionsInProgressCount > 1) { + ariaLabel = localize('chatSessionsInProgressStatus', "{0} chat sessions in progress", chatSessionsInProgressCount); + } else { + ariaLabel = localize('chatSessionInProgressStatus', "1 chat session in progress"); + } } // Signed out else if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { const signedOutWarning = localize('notSignedIntoCopilot', "Signed out"); - text = `$(copilot-not-connected) ${signedOutWarning}`; + text = `${this.chatEntitlementService.anonymous ? '$(copilot)' : '$(copilot-not-connected)'} ${signedOutWarning}`; ariaLabel = signedOutWarning; kind = 'prominent'; } @@ -236,25 +248,17 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu // Completions Disabled else if (this.editorService.activeTextEditorLanguageId && !isCompletionsEnabled(this.configurationService, this.editorService.activeTextEditorLanguageId)) { - text = `$(copilot-unavailable)`; + text = '$(copilot-unavailable)'; ariaLabel = localize('completionsDisabledStatus', "Code completions disabled"); } // Completions Snoozed else if (this.completionsService.isSnoozing()) { - text = `$(copilot-snooze)`; + text = '$(copilot-snooze)'; ariaLabel = localize('completionsSnoozedStatus', "Code completions snoozed"); } } - // Show progress indicator when chat sessions are in progress - if (hasInProgressSessions) { - text = `$(loading~spin)\u00A0${text}`; - // Update aria label to include progress information - const sessionCount = inProgress.reduce((total, item) => total + item.count, 0); - ariaLabel = `${ariaLabel}, ${sessionCount} chat session${sessionCount === 1 ? '' : 's'} in progress`; - } - const baseResult = { name: localize('chatStatus', "Copilot Status"), text, @@ -345,7 +349,8 @@ class ChatStatusDashboard extends Disposable { @ITelemetryService private readonly telemetryService: ITelemetryService, @ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService, @IInlineCompletionsService private readonly inlineCompletionsService: IInlineCompletionsService, - @IChatSessionsService private readonly chatSessionsService: IChatSessionsService + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); } @@ -414,12 +419,24 @@ class ChatStatusDashboard extends Disposable { })(); } + // Anonymous Indicator + else if (this.chatEntitlementService.anonymous && this.chatEntitlementService.sentiment.installed) { + addSeparator(localize('anonymousTitle', "Copilot Usage")); + + this.createQuotaIndicator(this.element, disposables, localize('quotaDisabled', "Disabled"), localize('completionsLabel', "Code completions"), false); // TODO@bpasero revisit this in the future when Completions are supported + this.createQuotaIndicator(this.element, disposables, localize('quotaLimited', "Limited"), localize('chatsLabel', "Chat messages"), false); + + this.element.appendChild($('div.description', undefined, localize('anonymousFooter', "Sign in to increase allowance."))); + } + // Chat sessions { let chatSessionsElement: HTMLElement | undefined; + const updateStatus = () => { const inProgress = this.chatSessionsService.getInProgress(); if (inProgress.some(item => item.count > 0)) { + addSeparator(localize('chatSessionsTitle', "Chat Sessions"), toAction({ id: 'workbench.view.chat.status.sessions', label: localize('viewChatSessionsLabel', "View Chat Sessions"), @@ -430,22 +447,17 @@ class ChatStatusDashboard extends Disposable { for (const { displayName, count } of inProgress) { if (count > 0) { - let lowerCaseName = displayName.toLocaleLowerCase(); - // Very specific case for providers that end in session/sessions to ensure we pluralize correctly - if (lowerCaseName.endsWith('session') || lowerCaseName.endsWith('sessions')) { - lowerCaseName = lowerCaseName.replace(/session$|sessions$/g, count > 1 ? 'sessions' : 'session'); - } - const text = localize('inProgressChatSession', "$(loading~spin) {0} {1} in progress", count, lowerCaseName); + const text = localize('inProgressChatSession', "$(loading~spin) {0} in progress", displayName); chatSessionsElement = this.element.appendChild($('div.description')); const parts = renderLabelWithIcons(text); chatSessionsElement.append(...parts); } } - } - else { + } else { chatSessionsElement?.remove(); } }; + updateStatus(); disposables.add(this.chatSessionsService.onDidChangeInProgress(updateStatus)); } @@ -497,14 +509,21 @@ class ChatStatusDashboard extends Disposable { // New to Copilot / Signed out { const newUser = isNewUser(this.chatEntitlementService); + const anonymousUser = this.chatEntitlementService.anonymous; const disabled = this.chatEntitlementService.sentiment.disabled || this.chatEntitlementService.sentiment.untrusted; const signedOut = this.chatEntitlementService.entitlement === ChatEntitlement.Unknown; if (newUser || signedOut || disabled) { addSeparator(); - let descriptionText: string; - if (newUser) { + let descriptionText: string | MarkdownString; + let descriptionClass = '.description'; + if (newUser && anonymousUser) { + descriptionText = new MarkdownString(localize({ key: 'activeDescriptionAnonymous', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3})", defaultChat.provider.default.name, defaultChat.provider.default.name, defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl), { isTrusted: true }); + descriptionClass = `${descriptionClass}.terms`; + } else if (newUser) { descriptionText = localize('activateDescription', "Set up Copilot to use AI features."); + } else if (anonymousUser) { + descriptionText = localize('enableMoreDescription', "Sign in to enable more Copilot AI features."); } else if (disabled) { descriptionText = localize('enableDescription', "Enable Copilot to use AI features."); } else { @@ -514,17 +533,31 @@ class ChatStatusDashboard extends Disposable { let buttonLabel: string; if (newUser) { buttonLabel = localize('activateCopilotButton', "Set up Copilot"); + } else if (anonymousUser) { + buttonLabel = localize('enableMoreCopilotButton', "Enable more AI Features"); } else if (disabled) { buttonLabel = localize('enableCopilotButton', "Enable Copilot"); } else { buttonLabel = localize('signInToUseCopilotButton', "Sign in to use Copilot"); } - this.element.appendChild($('div.description', undefined, descriptionText)); + let commandId: string; + if (newUser && anonymousUser) { + commandId = 'workbench.action.chat.triggerSetupAnonymously'; + } else { + commandId = 'workbench.action.chat.triggerSetup'; + } + + if (typeof descriptionText === 'string') { + this.element.appendChild($(`div${descriptionClass}`, undefined, descriptionText)); + } else { + const markdown = this.instantiationService.createInstance(MarkdownRenderer, {}); + this.element.appendChild($(`div${descriptionClass}`, undefined, disposables.add(markdown.render(descriptionText)).element)); + } const button = disposables.add(new Button(this.element, { ...defaultButtonStyles, hoverDelegate: nativeHoverDelegate })); button.label = buttonLabel; - disposables.add(button.onDidClick(() => this.runCommandAndClose('workbench.action.chat.triggerSetup'))); + disposables.add(button.onDidClick(() => this.runCommandAndClose(commandId))); } } @@ -579,18 +612,18 @@ class ChatStatusDashboard extends Disposable { } } - private runCommandAndClose(commandOrFn: string | Function): void { + private runCommandAndClose(commandOrFn: string | Function, ...args: any[]): void { if (typeof commandOrFn === 'function') { - commandOrFn(); + commandOrFn(...args); } else { this.telemetryService.publicLog2('workbenchActionExecuted', { id: commandOrFn, from: 'chat-status' }); - this.commandService.executeCommand(commandOrFn); + this.commandService.executeCommand(commandOrFn, ...args); } this.hoverService.hideHover(true); } - private createQuotaIndicator(container: HTMLElement, disposables: DisposableStore, quota: IQuotaSnapshot, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot) => void { + private createQuotaIndicator(container: HTMLElement, disposables: DisposableStore, quota: IQuotaSnapshot | string, label: string, supportsOverage: boolean): (quota: IQuotaSnapshot | string) => void { const quotaValue = $('span.quota-value'); const quotaBit = $('div.quota-bit'); const overageLabel = $('span.overage-label'); @@ -614,18 +647,20 @@ class ChatStatusDashboard extends Disposable { disposables.add(manageOverageButton.onDidClick(() => this.runCommandAndClose(() => this.openerService.open(URI.parse(defaultChat.manageOverageUrl))))); } - const update = (quota: IQuotaSnapshot) => { + const update = (quota: IQuotaSnapshot | string) => { quotaIndicator.classList.remove('error'); quotaIndicator.classList.remove('warning'); let usedPercentage: number; - if (quota.unlimited) { + if (typeof quota === 'string' || quota.unlimited) { usedPercentage = 0; } else { usedPercentage = Math.max(0, 100 - quota.percentRemaining); } - if (quota.unlimited) { + if (typeof quota === 'string') { + quotaValue.textContent = quota; + } else if (quota.unlimited) { quotaValue.textContent = localize('quotaUnlimited', "Included"); } else if (quota.overageCount) { quotaValue.textContent = localize('quotaDisplayWithOverage', "+{0} requests", this.quotaOverageFormatter.value.format(quota.overageCount)); @@ -642,7 +677,7 @@ class ChatStatusDashboard extends Disposable { } if (supportsOverage) { - if (quota.overageEnabled) { + if (typeof quota !== 'string' && quota?.overageEnabled) { overageLabel.textContent = localize('additionalUsageEnabled', "Additional paid premium requests enabled."); } else { overageLabel.textContent = localize('additionalUsageDisabled', "Additional paid premium requests disabled."); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index b73565e5c61..a8f807f32aa 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -6,17 +6,19 @@ import * as dom from '../../../../base/browser/dom.js'; import { IMouseWheelEvent } from '../../../../base/browser/mouseEvent.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; +import { IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { ITreeContextMenuEvent, ITreeElement } from '../../../../base/browser/ui/tree/tree.js'; -import { disposableTimeout, timeout } from '../../../../base/common/async.js'; +import { disposableTimeout, RunOnceScheduler, timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; +import { fromNow, fromNowByDay } from '../../../../base/common/date.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; -import { combinedDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { combinedDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceSet } from '../../../../base/common/map.js'; import { Schemas } from '../../../../base/common/network.js'; import { autorun, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; @@ -26,27 +28,37 @@ import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; import { localize } from '../../../../nls.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { fromNowByDay, fromNow } from '../../../../base/common/date.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IHoverService, WorkbenchHoverDelegate } from '../../../../platform/hover/browser/hover.js'; +import { IHoverOptions } from '../../../../base/browser/ui/hover/hover.js'; +import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; +import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; +import { ViewContainerLocation } from '../../../common/views.js'; import { ITextResourceEditorInput } from '../../../../platform/editor/common/editor.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { WorkbenchObjectTree, WorkbenchList } from '../../../../platform/list/browser/listService.js'; -import { IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; +import { WorkbenchList, WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; +import product from '../../../../platform/product/common/product.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; +import { EditorResourceAccessor } from '../../../../workbench/common/editor.js'; +import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; import { checkModeOption } from '../common/chat.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentService } from '../common/chatAgents.js'; -import { ChatContextKeys } from '../common/chatContextKeys.js'; +import { ChatContextKeys, ChatContextKeyExprs } from '../common/chatContextKeys.js'; import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, ModifiedFileEntryState } from '../common/chatEditingService.js'; import { IChatLayoutService } from '../common/chatLayoutService.js'; import { IChatModel, IChatResponseModel } from '../common/chatModel.js'; @@ -64,27 +76,22 @@ import { ChatAgentLocation, ChatConfiguration, ChatModeKind, TodoListWidgetPosit import { ILanguageModelToolsService, IToolData, ToolSet } from '../common/languageModelToolsService.js'; import { ComputeAutomaticInstructions } from '../common/promptSyntax/computeAutomaticInstructions.js'; import { PromptsConfig } from '../common/promptSyntax/config/config.js'; -import { type TPromptMetadata } from '../common/promptSyntax/parsers/promptHeader/promptHeader.js'; import { PromptsType } from '../common/promptSyntax/promptTypes.js'; -import { IPromptParserResult, IPromptsService } from '../common/promptSyntax/service/promptsService.js'; +import { ParsedPromptFile, PromptHeader } from '../common/promptSyntax/service/newPromptsParser.js'; +import { IPromptsService } from '../common/promptSyntax/service/promptsService.js'; import { handleModeSwitch } from './actions/chatActions.js'; -import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions, ChatViewId } from './chat.js'; +import { ChatTreeItem, ChatViewId, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from './chat.js'; import { ChatAccessibilityProvider } from './chatAccessibilityProvider.js'; import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { ChatTodoListWidget } from './chatContentParts/chatTodoListWidget.js'; import { ChatInputPart, IChatInputStyles } from './chatInputPart.js'; import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js'; import { ChatEditorOptions } from './chatOptions.js'; +import { ChatViewPane } from './chatViewPane.js'; import './media/chat.css'; import './media/chatAgentHover.css'; import './media/chatViewWelcome.css'; import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from './viewsWelcome/chatViewWelcomeController.js'; -import { ChatViewPane } from './chatViewPane.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import product from '../../../../platform/product/common/product.js'; -import { ChatEntitlement, IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; const $ = dom.$; @@ -160,12 +167,40 @@ interface IChatHistoryTemplate { disposables: DisposableStore; } +class ChatHistoryHoverDelegate extends WorkbenchHoverDelegate { + constructor( + private readonly getViewContainerLocation: () => ViewContainerLocation, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IConfigurationService configurationService: IConfigurationService, + @IHoverService hoverService: IHoverService, + ) { + super('element', { + instantHover: true + }, () => this.getHoverOptions(), configurationService, hoverService); + } + + private getHoverOptions(): Partial { + const sideBarPosition = this.layoutService.getSideBarPosition(); + const viewContainerLocation = this.getViewContainerLocation(); + + let hoverPosition: HoverPosition; + if (viewContainerLocation === ViewContainerLocation.Sidebar) { + hoverPosition = sideBarPosition === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; + } else if (viewContainerLocation === ViewContainerLocation.AuxiliaryBar) { + hoverPosition = sideBarPosition === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; + } else { + hoverPosition = HoverPosition.RIGHT; + } + + return { additionalClasses: ['chat-history-item-hover'], position: { hoverPosition, forcePosition: true } }; + } +} + class ChatHistoryListRenderer implements IListRenderer { readonly templateId = 'chatHistoryItem'; constructor( private readonly onDidClickItem: (item: IChatHistoryListItem) => void, - private readonly hoverService: IHoverService, private readonly formatHistoryTimestamp: (timestamp: number, todayMidnightMs: number) => string, private readonly todayMidnightMs: number ) { } @@ -186,25 +221,12 @@ class ChatHistoryListRenderer implements IListRenderer { this.onDidClickItem(element); })); @@ -274,6 +296,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private readonly inputPartDisposable: MutableDisposable = this._register(new MutableDisposable()); private readonly inlineInputPartDisposable: MutableDisposable = this._register(new MutableDisposable()); + private readonly timeoutDisposable: MutableDisposable = this._register(new MutableDisposable()); private inputContainer!: HTMLElement; private focusedInputDOM!: HTMLElement; private editorOptions!: ChatEditorOptions; @@ -284,6 +307,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private listContainer!: HTMLElement; private container!: HTMLElement; + private historyListContainer!: HTMLElement; get domNode() { return this.container; } @@ -320,6 +344,9 @@ export class ChatWidget extends Disposable implements IChatWidget { private readonly viewModelDisposables = this._register(new DisposableStore()); private _viewModel: ChatViewModel | undefined; + // Welcome view rendering scheduler to prevent reentrant calls + private _welcomeRenderScheduler: RunOnceScheduler; + // Coding agent locking state private _lockedToCodingAgent: string | undefined; private _lockedToCodingAgentContextKey!: IContextKey; @@ -331,6 +358,10 @@ export class ChatWidget extends Disposable implements IChatWidget { // Cache for prompt file descriptions to avoid async calls during rendering private readonly promptDescriptionsCache = new Map(); + // UI state for temporarily hiding chat history + private _historyVisible = true; + private _mostRecentlyFocusedItemIndex: number = -1; + private set viewModel(viewModel: ChatViewModel | undefined) { if (this._viewModel === viewModel) { return; @@ -391,11 +422,6 @@ export class ChatWidget extends Disposable implements IChatWidget { readonly viewContext: IChatWidgetViewContext; - private readonly chatSetupTriggerContext = ContextKeyExpr.or( - ChatContextKeys.Setup.installed.negate(), - ChatContextKeys.Entitlement.canSignUp - ); - get supportsChangingModes(): boolean { return !!this.viewOptions.supportsChangingModes; } @@ -404,12 +430,17 @@ export class ChatWidget extends Disposable implements IChatWidget { return localize('chatDisclaimer', "AI responses may be inaccurate."); } + get locationData() { + return this._location.resolveData?.(); + } + constructor( location: ChatAgentLocation | IChatWidgetLocationOptions, _viewContext: IChatWidgetViewContext | undefined, private readonly viewOptions: IChatWidgetViewOptions, private readonly styles: IChatWidgetStyles, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -427,10 +458,11 @@ export class ChatWidget extends Disposable implements IChatWidget { @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IChatModeService private readonly chatModeService: IChatModeService, - @IHoverService private readonly hoverService: IHoverService, @IChatTodoListService private readonly chatTodoListService: IChatTodoListService, @IChatLayoutService private readonly chatLayoutService: IChatLayoutService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, + @ICommandService private readonly commandService: ICommandService, + @IHoverService private readonly hoverService: IHoverService, ) { super(); this._lockedToCodingAgentContextKey = ChatContextKeys.lockedToCodingAgent.bindTo(this.contextKeyService); @@ -453,23 +485,17 @@ export class ChatWidget extends Disposable implements IChatWidget { // Context key for when empty state history is enabled and in empty state this.inEmptyStateWithHistoryEnabledKey = ChatContextKeys.inEmptyStateWithHistoryEnabled.bindTo(contextKeyService); + this._welcomeRenderScheduler = this._register(new RunOnceScheduler(() => this.renderWelcomeViewContentIfNeeded(), 10)); this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(ChatConfiguration.EmptyStateHistoryEnabled)) { this.updateEmptyStateWithHistoryContext(); - this.renderWelcomeViewContentIfNeeded(); + this._welcomeRenderScheduler.schedule(); } })); this.updateEmptyStateWithHistoryContext(); - // Update welcome view content when `anonymous` entitlement changes - let anonymousUsage = this.chatEntitlementService.entitlement === ChatEntitlement.Unknown; - this._register(this.chatEntitlementService.onDidChangeEntitlement(() => { - const newAnonymousUsage = this.chatEntitlementService.entitlement === ChatEntitlement.Unknown; - if (newAnonymousUsage !== anonymousUsage) { - anonymousUsage = newAnonymousUsage; - this.renderWelcomeViewContentIfNeeded(); - } - })); + // Update welcome view content when `anonymous` condition changes + this._register(this.chatEntitlementService.onDidChangeAnonymous(() => this._welcomeRenderScheduler.schedule())); this._register(bindContextKey(decidedChatEditingResourceContextKey, contextKeyService, (reader) => { const currentSession = this._editingSession.read(reader); @@ -628,7 +654,7 @@ export class ChatWidget extends Disposable implements IChatWidget { ChatContextKeys.Entitlement.canSignUp.key ]))) { // reset the input in welcome view if it was rendered in experimental mode - if (this.container.classList.contains('new-welcome-view') && !this.contextKeyService.contextMatchesRules(this.chatSetupTriggerContext)) { + if (this.container.classList.contains('new-welcome-view') && !this.contextKeyService.contextMatchesRules(ChatContextKeyExprs.chatSetupTriggerContext)) { this.container.classList.remove('new-welcome-view'); const renderFollowups = this.viewOptions.renderFollowups ?? false; const renderStyle = this.viewOptions.renderStyle; @@ -724,7 +750,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.createInput(this.container, { renderFollowups, renderStyle }); } - this.renderWelcomeViewContentIfNeeded(); + this._welcomeRenderScheduler.schedule(); this.createList(this.listContainer, { editable: !isInlineChat(this) && !isQuickChat(this), ...this.viewOptions.rendererOptions, renderStyle }); const scrollDownButton = this._register(new Button(this.listContainer, { @@ -864,6 +890,18 @@ export class ChatWidget extends Disposable implements IChatWidget { // Unlock coding agent when clearing this.unlockFromCodingAgent(); this._onDidClear.fire(); + this.chatTodoListWidget.clear(this.viewModel?.sessionId, true); + } + + public toggleHistoryVisibility(): void { + this._historyVisible = !this._historyVisible; + // Find and hide/show the existing history section via CSS class toggles + const historyRoot = this.welcomeMessageContainer.querySelector('.chat-welcome-history-root'); + if (historyRoot) { + historyRoot.classList.toggle('chat-welcome-history-hidden', !this._historyVisible); + } + const shouldShowHistory = this._historyVisible && !!historyRoot; + this.welcomeMessageContainer.classList.toggle('has-chat-history', shouldShowHistory); } private onDidChangeItems(skipDynamicLayout?: boolean) { @@ -888,10 +926,15 @@ export class ChatWidget extends Disposable implements IChatWidget { const renderStyle = this.viewOptions.renderStyle; this.createInput(this.container, { renderFollowups, renderStyle }); this.input.setChatMode(this.lastWelcomeViewChatMode ?? ChatModeKind.Ask); + this.focusInput(); } - this.renderWelcomeViewContentIfNeeded(); - this.renderChatTodoListWidget(); + if (treeItems.length > 0) { + this.updateChatViewVisibility(); + this.renderChatTodoListWidget(); + } else { + this._welcomeRenderScheduler.schedule(); + } this._onWillMaybeChangeHeight.fire(); @@ -933,6 +976,27 @@ export class ChatWidget extends Disposable implements IChatWidget { } } + /** + * Updates the DOM visibility of welcome view and chat list immediately + * @internal + */ + private updateChatViewVisibility(): void { + if (!this.viewModel) { + return; + } + + const numItems = this.viewModel.getItems().length; + dom.setVisibility(numItems === 0, this.welcomeMessageContainer); + dom.setVisibility(numItems !== 0, this.listContainer); + } + + /** + * Renders the welcome view content when needed. + * + * Note: Do not call this method directly. Instead, use `this._welcomeRenderScheduler.schedule()` + * to ensure proper debouncing and avoid potential cyclic calls + * @internal + */ private renderWelcomeViewContentIfNeeded() { if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') { @@ -954,14 +1018,12 @@ export class ChatWidget extends Disposable implements IChatWidget { `command:${generateInstructionsCommand}` ), { isTrusted: { enabledCommands: [generateInstructionsCommand] } }); } - if (this.contextKeyService.contextMatchesRules(this.chatSetupTriggerContext)) { + if (this.contextKeyService.contextMatchesRules(ChatContextKeyExprs.chatSetupTriggerContext)) { welcomeContent = this.getNewWelcomeViewContent(); this.container.classList.add('new-welcome-view'); - } - else if (expEmptyState) { + } else if (expEmptyState) { welcomeContent = this.getWelcomeViewContent(additionalMessage, expEmptyState); - } - else { + } else { const tips = this.input.currentModeKind === ChatModeKind.Ask ? new MarkdownString(localize('chatWidget.tips', "{0} or type {1} to attach context\n\n{2} to chat with extensions\n\nType {3} to use commands", '$(attach)', '#', '$(mention)', '/'), { supportThemeIcons: true }) : new MarkdownString(localize('chatWidget.tips.withoutParticipants', "{0} or type {1} to attach context", '$(attach)', '#'), { supportThemeIcons: true }); @@ -972,12 +1034,14 @@ export class ChatWidget extends Disposable implements IChatWidget { this.historyViewStore.clear(); dom.clearNode(this.welcomeMessageContainer); + // Reset history list reference when clearing welcome view + this.historyList = undefined; + // Optional: recent chat history above welcome content when enabled const showHistory = this.configurationService.getValue(ChatConfiguration.EmptyStateHistoryEnabled); - if (showHistory && !this._lockedToCodingAgent) { + if (showHistory && !this._lockedToCodingAgent && this._historyVisible) { this.renderWelcomeHistorySection(); } - this.welcomePart.value = this.instantiationService.createInstance( ChatViewWelcomePart, welcomeContent, @@ -987,12 +1051,28 @@ export class ChatWidget extends Disposable implements IChatWidget { } ); dom.append(this.welcomeMessageContainer, this.welcomePart.value.element); + + // Add right-click context menu to the entire welcome container + this._register(dom.addDisposableListener(this.welcomeMessageContainer, dom.EventType.CONTEXT_MENU, (e) => { + e.preventDefault(); + e.stopPropagation(); + this.contextMenuService.showContextMenu({ + menuId: MenuId.ChatWelcomeHistoryContext, + menuActionOptions: { shouldForwardArgs: true }, + contextKeyService: this.contextKeyService.createOverlay([ + ['chatHistoryVisible', this._historyVisible] + ]), + getAnchor: () => ({ x: e.clientX, y: e.clientY }), + getActionsContext: () => ({}) + }); + })); } } - if (this.viewModel) { - dom.setVisibility(numItems === 0, this.welcomeMessageContainer); - dom.setVisibility(numItems !== 0, this.listContainer); + this.updateChatViewVisibility(); + + if (numItems === 0) { + this.refreshHistoryList(); } } @@ -1007,85 +1087,42 @@ export class ChatWidget extends Disposable implements IChatWidget { try { const historyRoot = dom.append(this.welcomeMessageContainer, $('.chat-welcome-history-root')); const container = dom.append(historyRoot, $('.chat-welcome-history')); - const header = dom.append(container, $('.chat-welcome-history-header')); - const headerTitle = dom.append(header, $('.chat-welcome-history-header-title')); - headerTitle.textContent = localize('chat.history.title', 'History'); - const headerActions = dom.append(header, $('.chat-welcome-history-header-actions')); - const items = await this.chatService.getHistory(); - const filtered = items - .filter(i => !i.isActive) - .sort((a, b) => (b.lastMessageDate ?? 0) - (a.lastMessageDate ?? 0)) - .slice(0, 3); - - // If no items to show, hide the entire chat history section - if (filtered.length === 0) { + const initialHistoryItems = await this.computeHistoryItems(); + if (initialHistoryItems.length === 0) { historyRoot.remove(); return; } - const showAllButton = dom.append(headerActions, $('.chat-welcome-history-show-all')); - showAllButton.classList.add('codicon', `codicon-${Codicon.history.id}`, 'chat-welcome-history-show-all'); - showAllButton.tabIndex = 0; - showAllButton.setAttribute('role', 'button'); - const showAllHover = localize('chat.history.showAllHover', 'Show history...'); - showAllButton.setAttribute('aria-label', showAllHover); - const showAllHoverText = dom.$('div.chat-history-button-hover'); - showAllHoverText.textContent = showAllHover; - - this.historyViewStore.add(this.hoverService.setupDelayedHover(showAllButton, { content: showAllHoverText, appearance: { showPointer: false, compact: true } })); - - this.historyViewStore.add(dom.addDisposableListener(showAllButton, dom.EventType.CLICK, e => { - e.preventDefault(); - e.stopPropagation(); - setTimeout(() => { - this.instantiationService.invokeFunction(accessor => accessor.get(ICommandService).executeCommand('workbench.action.chat.history')); - }, 0); - })); - - this.historyViewStore.add(dom.addStandardDisposableListener(showAllButton, dom.EventType.KEY_DOWN, e => { - if (e.equals(KeyCode.Enter) || e.equals(KeyCode.Space)) { - e.preventDefault(); - e.stopPropagation(); - setTimeout(() => { - this.instantiationService.invokeFunction(accessor => accessor.get(ICommandService).executeCommand('workbench.action.chat.history')); - }, 0); - } - })); - const welcomeHistoryContainer = dom.append(container, $('.chat-welcome-history-list')); - - this.welcomeMessageContainer.classList.toggle('has-chat-history', filtered.length > 0); + this.historyListContainer = dom.append(container, $('.chat-welcome-history-list')); + historyRoot.classList.toggle('chat-welcome-history-hidden', !this._historyVisible); + this.welcomeMessageContainer.classList.toggle('has-chat-history', this._historyVisible && initialHistoryItems.length > 0); // Compute today's midnight once for label decisions const todayMidnight = new Date(); todayMidnight.setHours(0, 0, 0, 0); const todayMidnightMs = todayMidnight.getTime(); - // Create WorkbenchList for chat history items (limit to top 3) - const historyItems: IChatHistoryListItem[] = filtered.slice(0, 3).map(item => ({ - sessionId: item.sessionId, - title: item.title, - lastMessageDate: typeof item.lastMessageDate === 'number' ? item.lastMessageDate : Date.now(), - isActive: item.isActive - })); - - const listHeight = historyItems.length * 22; - welcomeHistoryContainer.style.height = `${listHeight}px`; - welcomeHistoryContainer.style.minHeight = `${listHeight}px`; - welcomeHistoryContainer.style.overflow = 'hidden'; + // Create hover delegate for proper tooltip positioning + const getViewContainerLocation = () => { + const panelLocation = this.contextKeyService.getContextKeyValue('chatPanelLocation'); + return panelLocation ?? ViewContainerLocation.AuxiliaryBar; + }; + const hoverDelegate = this.instantiationService.createInstance(ChatHistoryHoverDelegate, getViewContainerLocation); if (!this.historyList) { const delegate = new ChatHistoryListDelegate(); - const renderer = new ChatHistoryListRenderer( + + const renderer = this.instantiationService.createInstance( + ChatHistoryListRenderer, async (item) => await this.openHistorySession(item.sessionId), - this.hoverService, (timestamp, todayMs) => this.formatHistoryTimestamp(timestamp, todayMs), todayMidnightMs ); - const list = this.instantiationService.createInstance( + this.historyList = this._register(this.instantiationService.createInstance( WorkbenchList, 'ChatHistoryList', - welcomeHistoryContainer, + this.historyListContainer, delegate, [renderer], { @@ -1101,24 +1138,76 @@ export class ChatWidget extends Disposable implements IChatWidget { getWidgetAriaLabel: () => localize('chat.history.list', 'Chat History') } } - ); - this.historyList = this._register(list); + )); + this.historyList.getHTMLElement().tabIndex = -1; } else { const currentHistoryList = this.historyList.getHTMLElement(); - if (currentHistoryList && currentHistoryList.parentElement !== welcomeHistoryContainer) { - welcomeHistoryContainer.appendChild(currentHistoryList); + if (currentHistoryList && currentHistoryList.parentElement !== this.historyListContainer) { + this.historyListContainer.appendChild(currentHistoryList); } } - this.historyList.splice(0, this.historyList.length, historyItems); - this.historyList.layout(undefined, listHeight); + this.renderHistoryItems(initialHistoryItems); - // Deprecated text link replaced by icon button in header + // Add "Chat history..." link at the end + const previousChatsLink = dom.append(container, $('.chat-welcome-history-more')); + previousChatsLink.textContent = localize('chat.history.showMore', 'Chat history...'); + previousChatsLink.setAttribute('role', 'button'); + previousChatsLink.setAttribute('tabindex', '0'); + previousChatsLink.setAttribute('aria-label', localize('chat.history.showMoreAriaLabel', 'Open chat history')); + + // Add hover tooltip for the link at the end of the list + const hoverContent = localize('chat.history.showMoreHover', 'Show chat history...'); + this._register(this.hoverService.setupManagedHover(hoverDelegate, previousChatsLink, hoverContent)); + + this._register(dom.addDisposableListener(previousChatsLink, dom.EventType.CLICK, (e) => { + e.preventDefault(); + e.stopPropagation(); + this.commandService.executeCommand('workbench.action.chat.history'); + })); + this._register(dom.addDisposableListener(previousChatsLink, dom.EventType.KEY_DOWN, (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.commandService.executeCommand('workbench.action.chat.history'); + } + })); } catch (err) { this.logService.error('Failed to render welcome history', err); } } + private async computeHistoryItems(): Promise { + try { + const items = await this.chatService.getHistory(); + return items + .filter(i => !i.isActive) + .sort((a, b) => (b.lastMessageDate ?? 0) - (a.lastMessageDate ?? 0)) + .slice(0, 3) + .map(item => ({ + sessionId: item.sessionId, + title: item.title, + lastMessageDate: typeof item.lastMessageDate === 'number' ? item.lastMessageDate : Date.now(), + isActive: item.isActive + })); + } catch (err) { + this.logService.error('Failed to compute chat history items', err); + return []; + } + } + + private renderHistoryItems(historyItems: IChatHistoryListItem[]): void { + if (!this.historyList) { + return; + } + const listHeight = historyItems.length * 22; + if (this.historyListContainer) { + this.historyListContainer.style.height = `${listHeight}px`; + this.historyListContainer.style.minHeight = `${listHeight}px`; + } + this.historyList.splice(0, this.historyList.length, historyItems); + this.historyList.layout(undefined, listHeight); + } + private formatHistoryTimestamp(last: number, todayMidnightMs: number): string { if (last > todayMidnightMs) { const diffMs = Date.now() - last; @@ -1139,6 +1228,16 @@ export class ChatWidget extends Disposable implements IChatWidget { } } + private async refreshHistoryList(): Promise { + const numItems = this.viewModel?.getItems().length ?? 0; + // Only refresh history list when in empty state (welcome view) and history list exists + if (numItems !== 0 || !this.historyList) { + return; + } + const historyItems = await this.computeHistoryItems(); + this.renderHistoryItems(historyItems); + } + private renderChatTodoListWidget(): void { const sessionId = this.viewModel?.sessionId; if (!sessionId) { @@ -1215,7 +1314,7 @@ export class ChatWidget extends Disposable implements IChatWidget { private getNewWelcomeViewContent(): IChatViewWelcomeContent { let additionalMessage: string | IMarkdownString | undefined = undefined; - if (this.chatEntitlementService.entitlement === ChatEntitlement.Unknown) { + if (this.chatEntitlementService.anonymous) { additionalMessage = new MarkdownString(localize({ key: 'settings', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "AI responses may be inaccurate.\nBy continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3}).", defaultChat.provider.default.name, defaultChat.provider.default.name, defaultChat.termsStatementUrl, defaultChat.privacyStatementUrl), { isTrusted: true }); } else { additionalMessage = localize('expChatAdditionalMessage', "AI responses may be inaccurate."); @@ -1266,8 +1365,12 @@ export class ChatWidget extends Disposable implements IChatWidget { } private getPromptFileSuggestions(): IChatSuggestedPrompts[] { + // Get the current workspace folder context if available + const activeEditor = this.editorService.activeEditor; + const resource = activeEditor ? EditorResourceAccessor.getOriginalUri(activeEditor) : undefined; + // Get the prompt file suggestions configuration - const suggestions = PromptsConfig.getPromptFilesRecommendationsValue(this.configurationService); + const suggestions = PromptsConfig.getPromptFilesRecommendationsValue(this.configurationService, resource); if (!suggestions) { return []; } @@ -1345,9 +1448,12 @@ export class ChatWidget extends Disposable implements IChatWidget { // Build the final result array for (const { promptName } of topPrompts) { const description = this.promptDescriptionsCache.get(promptName); + const commandLabel = localize('chatWidget.promptFile.commandLabel', "/{0}", promptName); + const descriptionText = description?.trim() ? description : undefined; result.push({ icon: Codicon.run, - label: description || localize('chatWidget.promptFile.suggestion', "/{0}", promptName), + label: commandLabel, + description: descriptionText, prompt: `/${promptName} ` }); } @@ -1360,21 +1466,24 @@ export class ChatWidget extends Disposable implements IChatWidget { // Get all available prompt files with their metadata const promptCommands = await this.promptsService.findPromptSlashCommands(); + let cacheUpdated = false; // Load descriptions only for the specified prompts for (const promptCommand of promptCommands) { if (promptNames.includes(promptCommand.command)) { try { if (promptCommand.promptPath) { - const parseResult = await this.promptsService.parse( + const parseResult = await this.promptsService.parseNew( promptCommand.promptPath.uri, - promptCommand.promptPath.type, CancellationToken.None ); - if (parseResult.metadata?.description) { - this.promptDescriptionsCache.set(promptCommand.command, parseResult.metadata.description); + const description = parseResult.header?.description; + if (description) { + this.promptDescriptionsCache.set(promptCommand.command, description); + cacheUpdated = true; } else { // Set empty string to indicate we've checked this prompt this.promptDescriptionsCache.set(promptCommand.command, ''); + cacheUpdated = true; } } } catch (error) { @@ -1382,12 +1491,15 @@ export class ChatWidget extends Disposable implements IChatWidget { this.logService.warn('Failed to parse prompt file for description:', promptCommand.command, error); // Set empty string to indicate we've checked this prompt this.promptDescriptionsCache.set(promptCommand.command, ''); + cacheUpdated = true; } } } - // Trigger a re-render of the welcome view to show the loaded descriptions - this.renderWelcomeViewContentIfNeeded(); + // Fire event to trigger a re-render of the welcome view only if cache was updated + if (cacheUpdated) { + this._welcomeRenderScheduler.schedule(); + } } catch (error) { this.logService.warn('Failed to load specific prompt descriptions:', error); } @@ -1424,13 +1536,13 @@ export class ChatWidget extends Disposable implements IChatWidget { this.input.setVisible(visible); if (visible) { - this._register(disposableTimeout(() => { + this.timeoutDisposable.value = disposableTimeout(() => { // Progressive rendering paused while hidden, so start it up again. // Do it after a timeout because the container is not visible yet (it should be but offsetHeight returns 0 here) if (this._visible) { this.onDidChangeItems(true); } - }, 0)); + }, 0); if (!wasVisible) { dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => { @@ -1542,6 +1654,18 @@ export class ChatWidget extends Disposable implements IChatWidget { listInactiveSelectionIconForeground: undefined, } })); + + this._register(this.tree.onDidChangeFocus(() => { + const focused = this.tree.getFocus(); + if (focused && focused.length > 0) { + const focusedItem = focused[0]; + const items = this.tree.getNode(null).children; + const idx = items.findIndex(i => i.element === focusedItem); + if (idx !== -1) { + this._mostRecentlyFocusedItemIndex = idx; + } + } + })); this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); this._register(this.tree.onDidChangeContentHeight(() => { @@ -1820,7 +1944,7 @@ export class ChatWidget extends Disposable implements IChatWidget { renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit', supportsChangingModes: this.viewOptions.supportsChangingModes, dndContainer: this.viewOptions.dndContainer, - widgetViewKindTag: this.getWidgetViewKindTag() + widgetViewKindTag: this.getWidgetViewKindTag(), }; if (this.viewModel?.editing) { @@ -1922,11 +2046,11 @@ export class ChatWidget extends Disposable implements IChatWidget { this._register(this.chatAgentService.onDidChangeAgents(() => { this.parsedChatRequest = undefined; // Tools agent loads -> welcome content changes - this.renderWelcomeViewContentIfNeeded(); + this._welcomeRenderScheduler.schedule(); })); this._register(this.input.onDidChangeCurrentChatMode(() => { this.lastWelcomeViewChatMode = this.input.currentModeKind; - this.renderWelcomeViewContentIfNeeded(); + this._welcomeRenderScheduler.schedule(); this.refreshParsedInput(); this.renderFollowups(); })); @@ -1973,6 +2097,9 @@ export class ChatWidget extends Disposable implements IChatWidget { this.historyList.setSelection([]); } + // Clear history view state when switching sessions to ensure fresh rendering + this.historyViewStore.clear(); + this._codeBlockModelCollection.clear(); this.container.setAttribute('data-session-id', model.sessionId); @@ -2031,8 +2158,8 @@ export class ChatWidget extends Disposable implements IChatWidget { if (e.kind === 'setAgent') { this._onDidChangeAgent.fire({ agent: e.agent, slashCommand: e.command }); } - if (e.kind === 'addRequest') { - this.chatTodoListWidget.clear(model.sessionId); + if (e.kind === 'addRequest' || e.kind === 'removeRequest') { + this.chatTodoListWidget.clear(model.sessionId, e.kind === 'removeRequest' /*force*/); } })); @@ -2060,6 +2187,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return; } + this._mostRecentlyFocusedItemIndex = items.indexOf(node); this.tree.setFocus([node.element]); this.tree.domFocus(); } @@ -2091,7 +2219,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this._codingAgentPrefix = `@${name} `; this._lockedAgentId = agentId; this._lockedToCodingAgentContextKey.set(true); - this.renderWelcomeViewContentIfNeeded(); + this._welcomeRenderScheduler.schedule(); this.renderer.updateOptions({ restorable: false, editable: false, noFooter: true, progressMessageAtBottomOfResponse: true }); this.tree.rerender(); } @@ -2104,7 +2232,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this._lockedToCodingAgentContextKey.set(false); // Explicitly update the DOM to reflect unlocked state - this.renderWelcomeViewContentIfNeeded(); + this._welcomeRenderScheduler.schedule(); // Reset to default placeholder if (this.viewModel) { @@ -2170,21 +2298,22 @@ export class ChatWidget extends Disposable implements IChatWidget { return undefined; } - private async _applyPromptFileIfSet(requestInput: IChatRequestInputOptions): Promise { + private async _applyPromptFileIfSet(requestInput: IChatRequestInputOptions): Promise { if (!PromptsConfig.enabled(this.configurationService)) { // if prompts are not enabled, we don't need to do anything return undefined; } - let parseResult: IPromptParserResult | undefined; + let parseResult: ParsedPromptFile | undefined; // first check if the input has a prompt slash command const agentSlashPromptPart = this.parsedInput.parts.find((r): r is ChatRequestSlashPromptPart => r instanceof ChatRequestSlashPromptPart); if (agentSlashPromptPart) { parseResult = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.slashPromptCommand, CancellationToken.None); if (parseResult) { - // add the prompt file to the context, but not sticky - const toolReferences = this.toolsService.toToolReferences(parseResult.variableReferences); + // add the prompt file to the context + const refs = parseResult.body?.variableReferences.map(({ name, offset }) => ({ name, range: new OffsetRange(offset, offset + name.length + 1) })) ?? []; + const toolReferences = this.toolsService.toToolReferences(refs); requestInput.attachedContext.insertFirst(toPromptFileVariableEntry(parseResult.uri, PromptFileVariableKind.PromptFile, undefined, true, toolReferences)); // remove the slash command from the input @@ -2195,7 +2324,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const uri = this._findPromptFileInContext(requestInput.attachedContext); if (uri) { try { - parseResult = await this.promptsService.parse(uri, PromptsType.prompt, CancellationToken.None); + parseResult = await this.promptsService.parseNew(uri, CancellationToken.None); } catch (error) { this.logService.error(`[_applyPromptFileIfSet] Failed to parse prompt file: ${uri}`, error); } @@ -2205,10 +2334,6 @@ export class ChatWidget extends Disposable implements IChatWidget { if (!parseResult) { return undefined; } - const meta = parseResult.metadata; - if (meta?.promptType !== PromptsType.prompt) { - return undefined; - } const input = requestInput.input.trim(); requestInput.input = `Follow instructions in [${basename(parseResult.uri)}](${parseResult.uri.toString()}).`; @@ -2216,10 +2341,9 @@ export class ChatWidget extends Disposable implements IChatWidget { // if the input is not empty, append it to the prompt requestInput.input += `\n${input}`; } - - await this._applyPromptMetadata(meta, requestInput); - - return parseResult; + if (parseResult.header) { + await this._applyPromptMetadata(parseResult.header, requestInput); + } } private async _acceptInput(query: { query: string } | undefined, options?: IChatAcceptInputOptions): Promise { @@ -2245,7 +2369,7 @@ export class ChatWidget extends Disposable implements IChatWidget { const requestId = this.chatAccessibilityService.acceptRequest(); const requestInputs: IChatRequestInputOptions = { input: !query ? editorValue : query.query, - attachedContext: this.input.getAttachedAndImplicitContext(this.viewModel.sessionId), + attachedContext: options?.enableImplicitContext === false ? this.input.getAttachedContext(this.viewModel.sessionId) : this.input.getAttachedAndImplicitContext(this.viewModel.sessionId), }; const isUserQuery = !query; @@ -2325,7 +2449,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this.currentRequest = result.responseCompletePromise.then(() => { const responses = this.viewModel?.getItems().filter(isResponseVM); const lastResponse = responses?.[responses.length - 1]; - this.chatAccessibilityService.acceptResponse(lastResponse, requestId, options?.isVoiceInput); + this.chatAccessibilityService.acceptResponse(this, this.container, lastResponse, requestId, options?.isVoiceInput); if (lastResponse?.result?.nextQuestion) { const { prompt, participant, command } = lastResponse.result.nextQuestion; const question = formatChatQuestion(this.chatAgentService, this.location, prompt, participant, command); @@ -2370,18 +2494,22 @@ export class ChatWidget extends Disposable implements IChatWidget { return this.renderer.getLastFocusedFileTreeForResponse(response); } - focusLastMessage(): void { + focusResponseItem(lastFocused?: boolean): void { if (!this.viewModel) { return; } - const items = this.tree.getNode(null).children; - const lastItem = items[items.length - 1]; - if (!lastItem) { + let item; + if (lastFocused) { + item = items[this._mostRecentlyFocusedItemIndex] ?? items[items.length - 1]; + } else { + item = items[items.length - 1]; + } + if (!item) { return; } - this.tree.setFocus([lastItem.element]); + this.tree.setFocus([item.element]); this.tree.domFocus(); } @@ -2558,12 +2686,14 @@ export class ChatWidget extends Disposable implements IChatWidget { this.agentInInput.set(!!currentAgent); } - private async _applyPromptMetadata(metadata: TPromptMetadata, requestInput: IChatRequestInputOptions): Promise { - - const { mode, tools, model } = metadata; + private async _applyPromptMetadata({ mode, tools, model }: PromptHeader, requestInput: IChatRequestInputOptions): Promise { const currentMode = this.input.currentModeObs.get(); + if (tools !== undefined && !mode && currentMode.kind !== ChatModeKind.Agent) { + mode = ChatModeKind.Agent; + } + // switch to appropriate chat mode if needed if (mode && mode !== currentMode.name) { // Find the mode object to get its kind diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 6009018710c..dc33dc50dad 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; +import { alert } from '../../../../base/browser/ui/aria/aria.js'; import { assertNever } from '../../../../base/common/assert.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { encodeBase64 } from '../../../../base/common/buffer.js'; @@ -33,6 +34,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { AccessibilityWorkbenchSettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ChatModel } from '../common/chatModel.js'; import { IVariableReference } from '../common/chatModes.js'; @@ -40,9 +42,8 @@ import { ChatToolInvocation } from '../common/chatProgressTypes/chatToolInvocati import { ConfirmedReason, IChatService, ToolConfirmKind } from '../common/chatService.js'; import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../common/chatVariableEntries.js'; import { ChatConfiguration } from '../common/constants.js'; -import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, stringifyPromptTsxPart, ToolDataSource, ToolSet, IToolAndToolSetEnablementMap } from '../common/languageModelToolsService.js'; +import { CountTokensCallback, createToolSchemaUri, ILanguageModelToolsService, IPreparedToolInvocation, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, IToolResultInputOutputDetails, stringifyPromptTsxPart, ToolDataSource, ToolSet } from '../common/languageModelToolsService.js'; import { getToolConfirmationAlert } from './chatAccessibilityProvider.js'; -import { alert } from '../../../../base/browser/ui/aria/aria.js'; const jsonSchemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -60,6 +61,8 @@ const enum AutoApproveStorageKeys { GlobalAutoApproveOptIn = 'chat.tools.global.autoApprove.optIn' } +const SkipAutoApproveConfirmationKey = 'vscode.chat.tools.global.autoApprove.testMode'; + export const globalAutoApproveDescription = localize2( { key: 'autoApprove2.markdown', @@ -322,7 +325,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo const prepared = await this.prepareToolInvocation(tool, dto, token); - toolInvocation = new ChatToolInvocation(prepared, tool.data, dto.callId); + toolInvocation = new ChatToolInvocation(prepared, tool.data, dto.callId, dto.fromSubAgent); trackedCall.invocation = toolInvocation; const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace); if (autoConfirmed) { @@ -446,7 +449,9 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } private informScreenReader(msg: string | IMarkdownString): void { - alert(typeof msg === 'string' ? msg : msg.value); + if (this._configurationService.getValue(AccessibilityWorkbenchSettingId.VerboseChatProgressUpdates)) { + alert(typeof msg === 'string' ? msg : msg.value); + } } private playAccessibilitySignal(toolInvocations: ChatToolInvocation[]): void { @@ -531,6 +536,10 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return true; } + if (this._contextKeyService.getContextKeyValue(SkipAutoApproveConfirmationKey) === true) { + return true; + } + const promptResult = await this._dialogService.prompt({ type: Severity.Warning, message: localize('autoApprove2.title', 'Enable global auto approve?'), @@ -587,58 +596,73 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } } - toToolEnablementMap(toolOrToolsetNames: Set): Record { - const result: Record = {}; - for (const tool of this._tools.values()) { - if (tool.data.toolReferenceName && toolOrToolsetNames.has(tool.data.toolReferenceName)) { - result[tool.data.id] = true; - } else { - result[tool.data.id] = false; - } - } - - for (const toolSet of this._toolSets) { - if (toolOrToolsetNames.has(toolSet.referenceName)) { - for (const tool of toolSet.getTools()) { - result[tool.id] = true; - } - } - } - - return result; - } - /** * Create a map that contains all tools and toolsets with their enablement state. * @param toolOrToolSetNames A list of tool or toolset names that are enabled. * @returns A map of tool or toolset instances to their enablement state. */ - toToolAndToolSetEnablementMap(enabledToolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap { - const toolOrToolSetNames = new Set(enabledToolOrToolSetNames); + toToolAndToolSetEnablementMap(enabledQualifiedToolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap { + const toolOrToolSetNames = new Set(enabledQualifiedToolOrToolSetNames); const result = new Map(); for (const tool of this.getTools()) { if (tool.canBeReferencedInPrompt) { - result.set(tool, toolOrToolSetNames.has(tool.toolReferenceName ?? tool.displayName)); + const enabled = toolOrToolSetNames.has(getToolReferenceName(tool)) || /* legacy */ toolOrToolSetNames.has(tool.toolReferenceName ?? tool.displayName); + result.set(tool, enabled); } } - for (const toolSet of this._toolSets) { - const enabled = toolOrToolSetNames.has(toolSet.referenceName); - result.set(toolSet, enabled); + for (const toolSet of this.toolSets.get()) { + const toolSetEnabled = toolOrToolSetNames.has(getToolSetReferenceName(toolSet)) || /* legacy */ toolOrToolSetNames.has(toolSet.referenceName); + result.set(toolSet, toolSetEnabled); for (const tool of toolSet.getTools()) { - result.set(tool, enabled || toolOrToolSetNames?.has(tool.toolReferenceName ?? tool.displayName)); + const enabled = toolSetEnabled || toolOrToolSetNames.has(getToolReferenceName(tool, toolSet)) || /* legacy */ toolOrToolSetNames.has(tool.toolReferenceName ?? tool.displayName); + result.set(tool, enabled); } - } return result; } - public toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] { - const toolsOrToolSetByName = new Map(); - for (const toolSet of this.toolSets.get()) { - toolsOrToolSetByName.set(toolSet.referenceName, toolSet); + toQualifiedToolNames(map: IToolAndToolSetEnablementMap): string[] { + const toolsCoveredBySets = new Set(); + for (const item of map.keys()) { + if (item instanceof ToolSet) { + for (const tool of item.getTools()) { + toolsCoveredBySets.add(tool); + } + } } + + const result: string[] = []; for (const tool of this.getTools()) { - toolsOrToolSetByName.set(tool.toolReferenceName ?? tool.displayName, tool); + if (map.get(tool) && !toolsCoveredBySets.has(tool)) { + result.push(getToolReferenceName(tool)); + } + } + for (const toolSet of this.toolSets.get()) { + if (map.get(toolSet)) { + result.push(getToolSetReferenceName(toolSet)); + } else { + for (const tool of toolSet.getTools()) { + if (map.get(tool)) { + result.push(getToolReferenceName(tool, toolSet)); + } + } + } + } + return result; + } + + toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] { + const toolsOrToolSetByName = new Map(); + for (const tool of this.getTools()) { + if (tool.canBeReferencedInPrompt) { + toolsOrToolSetByName.set(getToolReferenceName(tool), tool); + } + } + for (const toolSet of this.toolSets.get()) { + toolsOrToolSetByName.set(getToolSetReferenceName(toolSet), toolSet); + for (const tool of toolSet.getTools()) { + toolsOrToolSetByName.set(getToolReferenceName(tool, toolSet), tool); + } } const result: ChatRequestToolReferenceEntry[] = []; @@ -695,6 +719,93 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo this._toolSets.add(result); return result; } + + *getQualifiedToolNames(): Iterable { + for (const tool of this.getTools()) { + if (tool.canBeReferencedInPrompt) { + yield getToolReferenceName(tool); + } + } + for (const toolSet of this.toolSets.get()) { + yield getToolSetReferenceName(toolSet); + for (const tool of toolSet.getTools()) { + yield getToolReferenceName(tool, toolSet); + } + } + } + + getDeprecatedQualifiedToolNames(): Map { + const result = new Map(); + const add = (name: string, qualifiedName: string) => { + if (name !== qualifiedName) { + result.set(name, qualifiedName); + } + }; + for (const tool of this.getTools()) { + if (tool.canBeReferencedInPrompt) { + add(tool.toolReferenceName ?? tool.displayName, getToolReferenceName(tool)); + } + } + for (const toolSet of this.toolSets.get()) { + add(toolSet.referenceName, getToolSetReferenceName(toolSet)); + for (const tool of toolSet.getTools()) { + add(tool.toolReferenceName ?? tool.displayName, getToolReferenceName(tool, toolSet)); + } + } + return result; + } + + getToolByQualifiedName(qualifiedName: string): IToolData | ToolSet | undefined { + for (const tool of this.getTools()) { + if (tool.canBeReferencedInPrompt) { + if (qualifiedName === getToolReferenceName(tool) || qualifiedName === (tool.toolReferenceName ?? tool.displayName) /* legacy */) { + if (matchesToolReferenceName(tool, qualifiedName)) { + return tool; + } + } + } + for (const toolSet of this.toolSets.get()) { + if (qualifiedName === getToolSetReferenceName(toolSet) || qualifiedName === toolSet.referenceName /* legacy */) { + return toolSet; + } + for (const tool of toolSet.getTools()) { + if (qualifiedName === getToolReferenceName(tool) || qualifiedName === (tool.toolReferenceName ?? tool.displayName) /* legacy */) { + return tool; + } + } + } + } + return undefined; + } + + getQualifiedToolName(tool: IToolData | ToolSet, toolSet?: ToolSet): string { + if (tool instanceof ToolSet) { + return getToolSetReferenceName(tool); + } + return getToolReferenceName(tool, toolSet); + } +} + +function getToolReferenceName(tool: IToolData, toolSet?: ToolSet) { + const toolName = tool.toolReferenceName ?? tool.displayName; + if (toolSet) { + return `${toolSet.referenceName}/${toolName}`; + } else if (tool.source.type === 'extension') { + return `${tool.source.extensionId.value.toLowerCase()}/${toolName}`; + } + return toolName; +} + +function getToolSetReferenceName(toolSet: ToolSet) { + if (toolSet.source.type === 'mcp') { + return `${toolSet.referenceName}/*`; + } + return toolSet.referenceName; +} + +function matchesToolReferenceName(tool: IToolData, name: string, toolSet?: ToolSet) { + const toolName = tool.toolReferenceName ?? tool.displayName; + return name === toolName || (toolSet && name === `${toolSet.referenceName}/${toolName}`); } type LanguageModelToolInvokedEvent = { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 67bcf14c5a8..8935b7c783f 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -564,6 +564,12 @@ } } +.interactive-item-container .value > .from-sub-agent.chat-tool-invocation-part, +.interactive-item-container .value > .from-sub-agent.chat-confirmation-widget, +.interactive-item-container .value > .from-sub-agent.chat-terminal-confirmation-widget { + margin-left: 18px; +} + .interactive-item-container .value > .rendered-markdown li > p { margin: 0; } @@ -1340,6 +1346,11 @@ have to be updated for changes to the rules above, or to support more deeply nes /* Have to override default styles which apply to all lists */ } +.chat-rate-limited-widget .codicon-info { + color: var(--vscode-notificationsInfoIcon-foreground) !important; + /* Have to override default styles which apply to all lists */ +} + .chat-notification-widget .chat-error-codicon .codicon-error, .interactive-response .interactive-response-error-details .codicon-error { color: var(--vscode-errorForeground) !important; @@ -1389,6 +1400,27 @@ have to be updated for changes to the rules above, or to support more deeply nes height: auto; } +.interactive-session .action-item.chat-attachment-button .action-label:not(.has-label) { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + padding: 0; + gap: 0; + border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); + border-radius: 4px; + box-sizing: border-box; + + .codicon { + width: 14px; + } +} + +.action-item.chat-attachment-button .codicon { + font-size: 14px; +} + .action-item.chat-mcp { display: flex !important; @@ -1651,6 +1683,20 @@ have to be updated for changes to the rules above, or to support more deeply nes min-height: 86px; } +.quick-input-widget .interactive-session .disclaimer { + margin: 8px 12px; + color: var(--vscode-descriptionForeground); + font-size: 12px; + + a { + color: var(--vscode-textLink-foreground); + } + + p { + margin: 0; + } +} + /* #endregion */ .interactive-response-progress-tree .monaco-list-row:not(.selected) .monaco-tl-row:hover { @@ -1834,7 +1880,8 @@ have to be updated for changes to the rules above, or to support more deeply nes .chat-notification-widget, .chat-summary-list, .chat-used-context-list, -.chat-quota-error-widget { +.chat-quota-error-widget, +.chat-rate-limited-widget { border: 1px solid var(--vscode-chat-requestBorder); border-radius: 4px; margin-bottom: 8px; @@ -2138,7 +2185,8 @@ have to be updated for changes to the rules above, or to support more deeply nes display: initial; } -.chat-quota-error-widget { +.chat-quota-error-widget, +.chat-rate-limited-widget { padding: 8px 12px; display: flex; gap: 6px; @@ -2148,7 +2196,8 @@ have to be updated for changes to the rules above, or to support more deeply nes padding: 2px 11px; } - .chat-quota-error-button { + .chat-quota-error-button, + .chat-rate-limited-button { margin-top: 6px; margin-bottom: 2px; } @@ -2168,7 +2217,8 @@ have to be updated for changes to the rules above, or to support more deeply nes margin-top: 2px; } - .chat-quota-error-message { + .chat-quota-error-message, + .chat-rate-limited-message { .rendered-markdown p { margin: 0px; } @@ -2495,6 +2545,8 @@ have to be updated for changes to the rules above, or to support more deeply nes top: 0; z-index: 2; background-color: var(--vscode-sideBar-background); + padding-left: 12px; + padding-right: 12px; } /* Special styling when todo widget is used inline in chat responses (subparts) */ @@ -2592,7 +2644,6 @@ have to be updated for changes to the rules above, or to support more deeply nes } .chat-todo-list-widget .todo-list-container { - padding-left: 10px; margin-top: 2px; max-height: calc(6.5 * 21px); /* 6.5 items to show half-line affordance */ @@ -2885,6 +2936,9 @@ have to be updated for changes to the rules above, or to support more deeply nes display: flex; align-items: center; width: 100%; + border: none; + outline: none; + padding-left: 10px; &:hover { background: var(--vscode-toolbar-hoverBackground); @@ -2937,6 +2991,10 @@ have to be updated for changes to the rules above, or to support more deeply nes &.finished:not(.collapsed) .chat-used-context-list.chat-thinking-collapsible { display: block; } + + &.finished .chat-thinking-fixed-header .codicon-pass-filled { + display: none; + } } /* item and dot rendering */ @@ -3008,3 +3066,24 @@ have to be updated for changes to the rules above, or to support more deeply nes .editor-instance .chat-todo-list-widget { background-color: var(--vscode-editor-background); } + +/* Show more attachments button styling */ +.chat-attachments-show-more-button { + opacity: 0.8; + transition: opacity 0.2s ease; +} + +.chat-attachments-show-more-button:hover { + opacity: 1; + background-color: var(--vscode-list-hoverBackground) !important; +} + +.chat-attachments-show-more-button:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.chat-attachments-show-more-button .chat-attached-context-custom-text { + font-style: italic; + color: var(--vscode-descriptionForeground); +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css b/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css index 66fda61efe5..5995a3e59f3 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditingEditorOverlay.css @@ -81,8 +81,6 @@ > .action-label:hover { color: var(--vscode-button-separator); opacity: 1; - height: 18px; - margin: 0px 4px !important; } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatEditorController.css b/src/vs/workbench/contrib/chat/browser/media/chatEditorController.css index ccef0cfb191..3c07814d88e 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatEditorController.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatEditorController.css @@ -15,7 +15,7 @@ } .chat-diff-change-content-widget .monaco-action-bar { - padding: 0; + padding: 2px; border-radius: 2px; background-color: var(--vscode-button-background); color: var(--vscode-button-foreground); @@ -30,11 +30,11 @@ .chat-diff-change-content-widget .monaco-action-bar .action-item .action-label.codicon { width: unset; padding: 2px; - font-size: 12px; - line-height: 14px; + font-size: 16px; + line-height: 16px; color: var(--vscode-button-foreground); } .chat-diff-change-content-widget .monaco-action-bar .action-item .action-label.codicon[class*='codicon-'] { - font-size: 12px; + font-size: 16px; } diff --git a/src/vs/workbench/contrib/chat/browser/media/chatSessions.css b/src/vs/workbench/contrib/chat/browser/media/chatSessions.css index b57f95250cd..db3744ca4a3 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatSessions.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatSessions.css @@ -8,6 +8,25 @@ background-color: var(--vscode-sideBarSectionHeader-background) !important; } +.chat-sessions-view { + display: flex; + flex-direction: column; + height: 100%; +} + +.chat-sessions-tree-container, +.getting-started-list-container { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; +} + +.chat-sessions-tree-container > .monaco-list, +.getting-started-list-container > .monaco-list { + flex: 1; +} + /* Style for empty state message */ .chat-sessions-message { padding: 20px; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index 05d5035fa47..14fdd0522b3 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -30,6 +30,13 @@ .chat-status-bar-entry-tooltip div.description { font-size: 11px; color: var(--vscode-descriptionForeground); + display: flex; + align-items: center; + gap: 3px; +} + +.chat-status-bar-entry-tooltip div.description.terms { + max-width: 250px; } .chat-status-bar-entry-tooltip .monaco-button { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css index 4fc3fee149b..3c2fd73d004 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css @@ -184,6 +184,7 @@ div.chat-welcome-view { color: var(--vscode-input-placeholderForeground); margin: 16px auto; padding: 0 12px; + padding-bottom: min(24px, max(0px, calc(24px - 1000 * (100% - 213px)))); a { color: var(--vscode-textLink-foreground); } @@ -209,29 +210,27 @@ div.chat-welcome-view { > .chat-welcome-view-suggested-prompt { display: flex; align-items: center; - padding: 2px; - border-radius: 8px; + gap: 6px; + height: 24px; + padding: 0 10px; + border-radius: 4px; background-color: var(--vscode-editorWidget-background); cursor: pointer; border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); - border-radius: 4px; - max-width: 200px; + max-width: 260px; width: fit-content; margin: 0 4px; - & > .chat-welcome-view-suggested-prompt-icon { - display: flex; - align-items: center; - font-size: 4px; - color: var(--vscode-icon-foreground) !important; - align-items: center; - padding: 4px; + & > .chat-welcome-view-suggested-prompt-title { + font-size: 14px; + font-weight: 600; + color: var(--vscode-editorWidget-foreground); + white-space: nowrap; } - & > .chat-welcome-view-suggested-prompt-label { - font-size: 14px; - color: var(--vscode-editorWidget-foreground); - padding: 4px 4px 4px 0; + & > .chat-welcome-view-suggested-prompt-description { + font-size: 13px; + color: var(--vscode-descriptionForeground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -247,20 +246,28 @@ div.chat-welcome-view { } } -/* Recent history list shown above the welcome content */ .chat-welcome-history-root { width: 100%; padding: 0px 8px 0 8px; + &.chat-welcome-history-hidden { + display: none; + } + .chat-welcome-history-header { display: flex; align-items: center; justify-content: space-between; - padding: 2px 4px 4px 4px; + padding: 2px 4px 2px 4px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; + min-height: 22px; color: var(--vscode-descriptionForeground); + + .chat-welcome-history-header-toolbar { + padding-right: 15px; + } } .chat-welcome-history-header-title { @@ -276,21 +283,6 @@ div.chat-welcome-view { padding-right: 16px; } - .chat-welcome-history-show-all { - cursor: pointer; - color: var(--vscode-icon-foreground); - padding: 2px; - border-radius: 4px; - } - - .chat-welcome-history-show-all:hover { - background: var(--vscode-toolbar-hoverBackground, var(--vscode-list-hoverBackground)); - } - - .chat-welcome-history-show-all:focus-visible { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: 1px; - } .chat-welcome-history { margin: 0 0 12px; @@ -313,11 +305,9 @@ div.chat-welcome-view { align-items: center; justify-content: space-between; padding: 2px 12px 4px 12px; - line-height: 18px; gap: 8px; - cursor: pointer; outline: none; - &:hover { background: var(--vscode-list-hoverBackground); } + .chat-welcome-history-title { font-size: 13px; white-space: nowrap; @@ -325,6 +315,7 @@ div.chat-welcome-view { text-overflow: ellipsis; flex: 1 1 auto; } + .chat-welcome-history-date { font-size: 11px; color: var(--vscode-descriptionForeground); @@ -334,9 +325,33 @@ div.chat-welcome-view { } .chat-welcome-history-more { - margin: 4px 0 0; - padding-left: 12px; - a { color: var(--vscode-textLink-foreground); cursor: pointer; } + color: var(--vscode-textLink-foreground); + text-decoration: none; + cursor: pointer; + padding: 2px 12px 4px 12px; + border: none; + background: none; + text-align: left; + height: 22px; + display: flex; + align-items: center; + border-radius: 4px; + box-sizing: border-box; + margin-right: 14px; + + &:hover { + background: var(--vscode-list-hoverBackground); + } + + &:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; + } } } +/* Chat history hover tooltip styling */ +.chat-history-item-hover { + max-width: 300px; + word-wrap: break-word; +} diff --git a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts index 51901ad6e45..dfb757db94c 100644 --- a/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/modelPicker/modelPickerActionItem.ts @@ -75,8 +75,8 @@ function getModelPickerActionBarActionProvider(commandService: ICommandService, }); } - // Add upgrade option if entitlement is free - if (chatEntitlementService.entitlement === ChatEntitlement.Free) { + // Add sign-in / upgrade option if entitlement is anonymous / free + if (chatEntitlementService.anonymous || chatEntitlementService.entitlement === ChatEntitlement.Free) { additionalActions.push({ id: 'moreModels', label: localize('chat.moreModels', "Add Premium Models"), @@ -84,7 +84,7 @@ function getModelPickerActionBarActionProvider(commandService: ICommandService, tooltip: localize('chat.moreModels.tooltip', "Add premium models"), class: undefined, run: () => { - const commandId = 'workbench.action.chat.upgradePlan'; + const commandId = chatEntitlementService.anonymous ? 'workbench.action.chat.triggerSetup' : 'workbench.action.chat.upgradePlan'; commandService.executeCommand(commandId); } }); diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts index c87e8d9c7e3..c8b543ef4f1 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptFileRewriter.ts @@ -9,13 +9,14 @@ import { ICodeEditorService } from '../../../../../editor/browser/services/codeE import { EditOperation } from '../../../../../editor/common/core/editOperation.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { ITextModel } from '../../../../../editor/common/model.js'; -import { IToolAndToolSetEnablementMap, IToolData, ToolSet } from '../../common/languageModelToolsService.js'; +import { ILanguageModelToolsService, IToolAndToolSetEnablementMap } from '../../common/languageModelToolsService.js'; import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js'; export class PromptFileRewriter { constructor( @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IPromptsService private readonly _promptsService: IPromptsService + @IPromptsService private readonly _promptsService: IPromptsService, + @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService ) { } @@ -26,30 +27,20 @@ export class PromptFileRewriter { } const model = editor.getModel(); - const parser = this._promptsService.getSyntaxParserFor(model); - await parser.start(token).settled(); - const { header } = parser; - if (header === undefined) { + const parser = this._promptsService.getParsedPromptFile(model); + if (!parser.header) { return undefined; } - const completed = await header.settled; - if (!completed || token.isCancellationRequested) { - return; + const toolsAttr = parser.header.getAttribute('tools'); + if (!toolsAttr) { + return undefined; } - if (('tools' in header.metadataUtility) === false) { - return undefined; - } - const { tools } = header.metadataUtility; - if (tools === undefined) { - return undefined; - } - editor.setSelection(tools.range); - this.rewriteTools(model, newTools, tools.range); + editor.setSelection(toolsAttr.range); + this.rewriteTools(model, newTools, toolsAttr.range); } - public rewriteTools(model: ITextModel, newTools: IToolAndToolSetEnablementMap | undefined, range: Range): void { const newString = newTools === undefined ? '' : `tools: ${this.getNewValueString(newTools)}`; model.pushStackElement(); @@ -58,24 +49,7 @@ export class PromptFileRewriter { } public getNewValueString(tools: IToolAndToolSetEnablementMap): string { - const newToolNames: string[] = []; - const toolsCoveredBySets = new Set(); - for (const [item, picked] of tools) { - if (picked && item instanceof ToolSet) { - for (const tool of item.getTools()) { - toolsCoveredBySets.add(tool); - } - } - } - for (const [item, picked] of tools) { - if (picked) { - if (item instanceof ToolSet) { - newToolNames.push(item.referenceName); - } else if (!toolsCoveredBySets.has(item)) { - newToolNames.push(item.toolReferenceName ?? item.displayName); - } - } - } + const newToolNames = this._languageModelToolsService.toQualifiedToolNames(tools); return `[${newToolNames.map(s => `'${s}'`).join(', ')}]`; } } diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts index 4b7832b0ee1..28f213135e1 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/promptToolsCodeLensProvider.ts @@ -15,10 +15,10 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { showToolsPicker } from '../actions/chatToolPicker.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; import { ALL_PROMPTS_LANGUAGE_SELECTOR } from '../../common/promptSyntax/promptTypes.js'; -import { PromptToolsMetadata } from '../../common/promptSyntax/parsers/promptHeader/metadata/tools.js'; import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js'; import { registerEditorFeature } from '../../../../../editor/common/editorFeatures.js'; import { PromptFileRewriter } from './promptFileRewriter.js'; +import { Range } from '../../../../../editor/common/core/range.js'; class PromptToolsCodeLensProvider extends Disposable implements CodeLensProvider { @@ -37,56 +37,47 @@ class PromptToolsCodeLensProvider extends Disposable implements CodeLensProvider this._register(this.languageService.codeLensProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, this)); this._register(CommandsRegistry.registerCommand(this.cmdId, (_accessor, ...args) => { - const [first, second] = args; - if (isITextModel(first) && second instanceof PromptToolsMetadata) { - this.updateTools(first, second); + const [first, second, third] = args; + if (isITextModel(first) && Range.isIRange(second) && Array.isArray(third)) { + this.updateTools(first, Range.lift(second), third); } })); } async provideCodeLenses(model: ITextModel, token: CancellationToken): Promise { - const parser = this.promptsService.getSyntaxParserFor(model); - - await parser.start(token).settled(); - const { header } = parser; - if (!header) { + const parser = this.promptsService.getParsedPromptFile(model); + if (!parser.header) { return undefined; } - const completed = await header.settled; - if (!completed || token.isCancellationRequested) { - return undefined; - } - if (('tools' in header.metadataUtility) === false) { - return undefined; - } - - const { tools } = header.metadataUtility; - if (tools === undefined) { + const toolsAttr = parser.header.getAttribute('tools'); + if (!toolsAttr || toolsAttr.value.type !== 'array') { return undefined; } + const items = toolsAttr.value.items; + const selectedTools = items.filter(item => item.type === 'string').map(item => item.value); const codeLens: CodeLens = { - range: tools.range.collapseToStart(), + range: toolsAttr.range.collapseToStart(), command: { title: localize('configure-tools.capitalized.ellipsis', "Configure Tools..."), id: this.cmdId, - arguments: [model, tools] + arguments: [model, toolsAttr.range, selectedTools] } }; return { lenses: [codeLens] }; } - private async updateTools(model: ITextModel, tools: PromptToolsMetadata) { + private async updateTools(model: ITextModel, range: Range, selectedTools: readonly string[]) { - const selectedToolsNow = () => tools.value ? this.languageModelToolsService.toToolAndToolSetEnablementMap(tools.value) : new Map(); + const selectedToolsNow = () => this.languageModelToolsService.toToolAndToolSetEnablementMap(selectedTools); const newSelectedAfter = await this.instantiationService.invokeFunction(showToolsPicker, localize('placeholder', "Select tools"), undefined, selectedToolsNow); if (!newSelectedAfter) { return; } - await this.instantiationService.createInstance(PromptFileRewriter).rewriteTools(model, newSelectedAfter, tools.range); + await this.instantiationService.createInstance(PromptFileRewriter).rewriteTools(model, newSelectedAfter, range); } } diff --git a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts index e1ca371981c..6c2a7ad4771 100644 --- a/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts +++ b/src/vs/workbench/contrib/chat/browser/viewsWelcome/chatViewWelcomeController.ts @@ -118,6 +118,7 @@ export interface IChatViewWelcomeContent { export interface IChatSuggestedPrompts { readonly icon?: ThemeIcon; readonly label: string; + readonly description?: string; readonly prompt: string; } @@ -195,14 +196,20 @@ export class ChatViewWelcomePart extends Disposable { // Make the prompt element keyboard accessible promptElement.setAttribute('role', 'button'); promptElement.setAttribute('tabindex', '0'); - promptElement.setAttribute('aria-label', localize('suggestedPromptAriaLabel', 'Suggested prompt: {0}', prompt.label)); - if (prompt.icon) { - const iconElement = dom.append(promptElement, $('.chat-welcome-view-suggested-prompt-icon')); - iconElement.appendChild(renderIcon(prompt.icon)); + const promptAriaLabel = prompt.description + ? localize('suggestedPromptAriaLabelWithDescription', 'Suggested prompt: {0}, {1}', prompt.label, prompt.description) + : localize('suggestedPromptAriaLabel', 'Suggested prompt: {0}', prompt.label); + promptElement.setAttribute('aria-label', promptAriaLabel); + const titleElement = dom.append(promptElement, $('.chat-welcome-view-suggested-prompt-title')); + titleElement.textContent = prompt.label; + const tooltip = localize('runPromptTitle', "Suggested prompt: {0}", prompt.prompt); + promptElement.title = tooltip; + titleElement.title = tooltip; + if (prompt.description) { + const descriptionElement = dom.append(promptElement, $('.chat-welcome-view-suggested-prompt-description')); + descriptionElement.textContent = prompt.description; + descriptionElement.title = prompt.description; } - const labelElement = dom.append(promptElement, $('.chat-welcome-view-suggested-prompt-label')); - labelElement.textContent = prompt.label; - labelElement.title = localize('runPromptTitle', "Suggested prompt: {0}", prompt.prompt); const executePrompt = () => { type SuggestedPromptClickEvent = { suggestedPrompt: string }; @@ -270,7 +277,10 @@ export class ChatViewWelcomePart extends Disposable { this.content.additionalMessage !== content.additionalMessage || this.content.tips?.value !== content.tips?.value || this.content.suggestedPrompts?.length !== content.suggestedPrompts?.length || - this.content.suggestedPrompts?.some((prompt, index) => content.suggestedPrompts?.[index]?.label !== prompt.label)); + this.content.suggestedPrompts?.some((prompt, index) => { + const incoming = content.suggestedPrompts?.[index]; + return incoming?.label !== prompt.label || incoming?.description !== prompt.description; + })); } private renderMarkdownMessageContent(renderer: MarkdownRenderer, content: IMarkdownString, options: IChatViewWelcomeRenderOptions | undefined): IMarkdownRenderResult { diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 45a8b1d2109..ad2eb8b8d4e 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -10,7 +10,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { revive } from '../../../../base/common/marshalling.js'; +import { revive, Revived } from '../../../../base/common/marshalling.js'; import { IObservable, observableValue } from '../../../../base/common/observable.js'; import { equalsIgnoreCase } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -141,7 +141,7 @@ export interface IChatAgentRequest { isParticipantDetected?: boolean; variables: IChatRequestVariableData; location: ChatAgentLocation; - locationData?: IChatLocationData; + locationData?: Revived; acceptedConfirmationData?: any[]; rejectedConfirmationData?: any[]; userSelectedModelId?: string; diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index a790d51d1bb..487740709d9 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -45,6 +45,8 @@ export namespace ChatContextKeys { * True when the chat widget is locked to the coding agent session. */ export const lockedToCodingAgent = new RawContextKey('lockedToCodingAgent', false, { type: 'boolean', description: localize('lockedToCodingAgent', "True when the chat widget is locked to the coding agent session.") }); + export const withinEditSessionDiff = new RawContextKey('withinEditSessionDiff', false, { type: 'boolean', description: localize('withinEditSessionDiff', "True when the chat widget dispatches to the edit session chat.") }); + export const filePartOfEditSession = new RawContextKey('filePartOfEditSession', false, { type: 'boolean', description: localize('filePartOfEditSession', "True when the chat widget is within a file with an edit session.") }); export const extensionParticipantRegistered = new RawContextKey('chatPanelExtensionParticipantRegistered', false, { type: 'boolean', description: localize('chatPanelExtensionParticipantRegistered', "True when a default chat participant is registered for the panel from an extension.") }); export const panelParticipantRegistered = new RawContextKey('chatPanelParticipantRegistered', false, { type: 'boolean', description: localize('chatParticipantRegistered', "True when a default chat participant is registered for the panel.") }); @@ -96,4 +98,12 @@ export namespace ChatContextKeyExprs { ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Edit), ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), ); + + /** + * Context expression that indicates when the welcome/setup view should be shown + */ + export const chatSetupTriggerContext = ContextKeyExpr.or( + ChatContextKeys.Setup.installed.negate(), + ChatContextKeys.Entitlement.canSignUp + ); } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 707f8f6a8c0..0366481090c 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -8,7 +8,7 @@ import { Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable, IReader } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; -import { TextEdit } from '../../../../editor/common/languages.js'; +import { Location, TextEdit } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { localize } from '../../../../nls.js'; import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; @@ -272,6 +272,7 @@ export interface IModifiedFileEntry { readonly linesRemoved?: IObservable; getEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration; + hasModificationAt(location: Location): boolean; } export interface IChatEditingSessionStream { diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index d86965601cb..0b45e54392d 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -215,14 +215,15 @@ const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { re export interface IChatRequestModeInfo { kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply' isBuiltin: boolean; - instructions: IChatRequestModeInstructions | undefined; + modeInstructions: IChatRequestModeInstructions | undefined; modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined; applyCodeBlockSuggestionId: EditSuggestionId | undefined; } export interface IChatRequestModeInstructions { - readonly content: string | undefined; - readonly toolReferences: readonly ChatRequestToolReferenceEntry[] | undefined; + readonly content: string; + readonly toolReferences: readonly ChatRequestToolReferenceEntry[]; + readonly metadata?: Record; } export interface IChatRequestModelParameters { @@ -1965,15 +1966,15 @@ export interface IChatAgentEditedFileEvent { export namespace ChatResponseResource { export const scheme = 'vscode-chat-response-resource'; - export function createUri(sessionId: string, requestId: string, toolCallId: string, index: number, basename?: string): URI { + export function createUri(sessionId: string, toolCallId: string, index: number, basename?: string): URI { return URI.from({ scheme: ChatResponseResource.scheme, authority: sessionId, - path: `/tool/${requestId}/${toolCallId}/${index}` + (basename ? `/${basename}` : ''), + path: `/tool/${toolCallId}/${index}` + (basename ? `/${basename}` : ''), }); } - export function parseUri(uri: URI): undefined | { sessionId: string; requestId: string; toolCallId: string; index: number } { + export function parseUri(uri: URI): undefined | { sessionId: string; toolCallId: string; index: number } { if (uri.scheme !== ChatResponseResource.scheme) { return undefined; } @@ -1983,14 +1984,13 @@ export namespace ChatResponseResource { return undefined; } - const [, kind, requestId, toolCallId, index] = parts; + const [, kind, toolCallId, index] = parts; if (kind !== 'tool') { return undefined; } return { sessionId: uri.authority, - requestId: requestId, toolCallId: toolCallId, index: Number(index), }; diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index 40b84d443a1..fba7f476521 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -98,8 +98,7 @@ export class ChatModeService extends Disposable implements IChatModeService { description: cachedMode.description, tools: cachedMode.customTools, model: cachedMode.model, - body: cachedMode.body || '', - variableReferences: cachedMode.variableReferences || [], + modeInstructions: cachedMode.modeInstructions ?? { content: cachedMode.body ?? '', toolReferences: [] }, }; const instance = new CustomChatMode(customChatMode); this._customModeInstances.set(uri.toString(), instance); @@ -200,8 +199,8 @@ export interface IChatModeData { readonly kind: ChatModeKind; readonly customTools?: readonly string[]; readonly model?: string; - readonly body?: string; - readonly variableReferences?: readonly IVariableReference[]; + readonly modeInstructions?: IChatModeInstructions; + readonly body?: string; /* deprecated */ readonly uri?: URI; } @@ -214,8 +213,7 @@ export interface IChatMode { readonly kind: ChatModeKind; readonly customTools?: IObservable; readonly model?: IObservable; - readonly body?: IObservable; - readonly variableReferences?: IObservable; + readonly modeInstructions?: IObservable; readonly uri?: IObservable; } @@ -224,6 +222,12 @@ export interface IVariableReference { readonly range: IOffsetRange; } +export interface IChatModeInstructions { + readonly content: string; + readonly toolReferences: readonly IVariableReference[]; + readonly metadata?: Record; +} + function isCachedChatModeData(data: unknown): data is IChatModeData { if (typeof data !== 'object' || data === null) { return false; @@ -235,8 +239,7 @@ function isCachedChatModeData(data: unknown): data is IChatModeData { typeof mode.kind === 'string' && (mode.description === undefined || typeof mode.description === 'string') && (mode.customTools === undefined || Array.isArray(mode.customTools)) && - (mode.body === undefined || typeof mode.body === 'string') && - (mode.variableReferences === undefined || Array.isArray(mode.variableReferences)) && + (mode.modeInstructions === undefined || (typeof mode.modeInstructions === 'object' && mode.modeInstructions !== null)) && (mode.model === undefined || typeof mode.model === 'string') && (mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null)); } @@ -244,8 +247,7 @@ function isCachedChatModeData(data: unknown): data is IChatModeData { export class CustomChatMode implements IChatMode { private readonly _descriptionObservable: ISettableObservable; private readonly _customToolsObservable: ISettableObservable; - private readonly _bodyObservable: ISettableObservable; - private readonly _variableReferencesObservable: ISettableObservable; + private readonly _modeInstructions: ISettableObservable; private readonly _uriObservable: ISettableObservable; private readonly _modelObservable: ISettableObservable; @@ -268,12 +270,8 @@ export class CustomChatMode implements IChatMode { return this._modelObservable; } - get body(): IObservable { - return this._bodyObservable; - } - - get variableReferences(): IObservable { - return this._variableReferencesObservable; + get modeInstructions(): IObservable { + return this._modeInstructions; } get uri(): IObservable { @@ -294,8 +292,7 @@ export class CustomChatMode implements IChatMode { this._descriptionObservable = observableValue('description', customChatMode.description); this._customToolsObservable = observableValue('customTools', customChatMode.tools); this._modelObservable = observableValue('model', customChatMode.model); - this._bodyObservable = observableValue('body', customChatMode.body); - this._variableReferencesObservable = observableValue('variableReferences', customChatMode.variableReferences); + this._modeInstructions = observableValue('_modeInstructions', customChatMode.modeInstructions); this._uriObservable = observableValue('uri', customChatMode.uri); } @@ -308,8 +305,7 @@ export class CustomChatMode implements IChatMode { this._descriptionObservable.set(newData.description, tx); this._customToolsObservable.set(newData.tools, tx); this._modelObservable.set(newData.model, tx); - this._bodyObservable.set(newData.body, tx); - this._variableReferencesObservable.set(newData.variableReferences, tx); + this._modeInstructions.set(newData.modeInstructions, tx); this._uriObservable.set(newData.uri, tx); }); } @@ -322,8 +318,7 @@ export class CustomChatMode implements IChatMode { kind: this.kind, customTools: this.customTools.get(), model: this.model.get(), - body: this.body.get(), - variableReferences: this.variableReferences.get(), + modeInstructions: this.modeInstructions.get(), uri: this.uri.get() }; } diff --git a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts index 7e0908d6338..bdba2e6820d 100644 --- a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts +++ b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts @@ -8,7 +8,7 @@ import { encodeBase64 } from '../../../../../base/common/buffer.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { localize } from '../../../../../nls.js'; -import { IChatExtensionsContent, IChatToolInputInvocationData, IChatTodoListContent, IChatToolInvocation, IChatToolInvocationSerialized, type IChatTerminalToolInvocationData, ConfirmedReason, ToolConfirmKind } from '../chatService.js'; +import { ConfirmedReason, IChatExtensionsContent, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind, type IChatTerminalToolInvocationData } from '../chatService.js'; import { IPreparedToolInvocation, isToolResultOutputDetails, IToolConfirmationMessages, IToolData, IToolProgressStep, IToolResult, ToolDataSource } from '../languageModelToolsService.js'; export class ChatToolInvocation implements IChatToolInvocation { @@ -45,12 +45,13 @@ export class ChatToolInvocation implements IChatToolInvocation { public readonly presentation: IPreparedToolInvocation['presentation']; public readonly toolId: string; public readonly source: ToolDataSource; + public readonly fromSubAgent: boolean | undefined; public readonly toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent; public readonly progress = observableValue<{ message?: string | IMarkdownString; progress: number }>(this, { progress: 0 }); - constructor(preparedInvocation: IPreparedToolInvocation | undefined, toolData: IToolData, public readonly toolCallId: string) { + constructor(preparedInvocation: IPreparedToolInvocation | undefined, toolData: IToolData, public readonly toolCallId: string, fromSubAgent: boolean | undefined) { const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${toolData.displayName}"`); const invocationMessage = preparedInvocation?.invocationMessage ?? defaultMessage; this.invocationMessage = invocationMessage; @@ -61,6 +62,7 @@ export class ChatToolInvocation implements IChatToolInvocation { this.toolSpecificData = preparedInvocation?.toolSpecificData; this.toolId = toolData.id; this.source = toolData.source; + this.fromSubAgent = fromSubAgent; if (!this._confirmationMessages) { // No confirmation needed @@ -113,6 +115,7 @@ export class ChatToolInvocation implements IChatToolInvocation { toolSpecificData: this.toolSpecificData, toolCallId: this.toolCallId, toolId: this.toolId, + fromSubAgent: this.fromSubAgent, }; } } diff --git a/src/vs/workbench/contrib/chat/common/chatResponseResourceFileSystemProvider.ts b/src/vs/workbench/contrib/chat/common/chatResponseResourceFileSystemProvider.ts index 212709d2bf1..ee8e6774a1d 100644 --- a/src/vs/workbench/contrib/chat/common/chatResponseResourceFileSystemProvider.ts +++ b/src/vs/workbench/contrib/chat/common/chatResponseResourceFileSystemProvider.ts @@ -84,27 +84,36 @@ export class ChatResponseResourceFileSystemProvider extends Disposable implement throw createFileSystemProviderError('fs is readonly', FileSystemProviderErrorCode.NoPermissions); } - private lookupURI(uri: URI): Uint8Array | Promise { + private findMatchingInvocation(uri: URI) { const parsed = ChatResponseResource.parseUri(uri); if (!parsed) { throw createFileSystemProviderError(`File not found`, FileSystemProviderErrorCode.FileNotFound); } - const { sessionId, requestId, toolCallId } = parsed; - const result = this.chatService.getSession(sessionId) - ?.getRequests() - .find(r => r.id === requestId) - ?.response?.entireResponse.value - .find((r): r is IChatToolInvocation | IChatToolInvocationSerialized => (r.kind === 'toolInvocation' || r.kind === 'toolInvocationSerialized') && r.toolCallId === toolCallId); - - if (!result) { + const { sessionId, toolCallId, index } = parsed; + const session = this.chatService.getSession(sessionId); + if (!session) { throw createFileSystemProviderError(`File not found`, FileSystemProviderErrorCode.FileNotFound); } + const requests = session.getRequests(); + for (let k = requests.length - 1; k >= 0; k--) { + const req = requests[k]; + const tc = req.response?.entireResponse.value.find((r): r is IChatToolInvocation | IChatToolInvocationSerialized => (r.kind === 'toolInvocation' || r.kind === 'toolInvocationSerialized') && r.toolCallId === toolCallId); + if (tc) { + return { result: tc, index }; + } + } + + throw createFileSystemProviderError(`File not found`, FileSystemProviderErrorCode.FileNotFound); + } + + private lookupURI(uri: URI): Uint8Array | Promise { + const { result, index } = this.findMatchingInvocation(uri); if (!isToolResultInputOutputDetails(result.resultDetails)) { throw createFileSystemProviderError(`Tool does not have I/O`, FileSystemProviderErrorCode.FileNotFound); } - const part = result.resultDetails.output.at(parsed.index); + const part = result.resultDetails.output.at(index); if (!part) { throw createFileSystemProviderError(`Tool does not have part`, FileSystemProviderErrorCode.FileNotFound); } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 06875920f15..92a00537d7e 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -19,6 +19,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { IWorkspaceSymbol } from '../../search/common/search.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentResult, UserSelectedTools } from './chatAgents.js'; +import { IChatEditingSession } from './chatEditingService.js'; import { ChatModel, IChatModel, IChatRequestModeInfo, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from './chatModel.js'; import { IParsedChatRequest } from './chatParserTypes.js'; import { IChatParserContext } from './chatRequestParser.js'; @@ -50,6 +51,7 @@ export interface IChatResponseErrorDetails { responseIsFiltered?: boolean; responseIsRedacted?: boolean; isQuotaExceeded?: boolean; + isRateLimited?: boolean; level?: ChatErrorLevel; confirmationButtons?: IChatResponseErrorDetailsConfirmationButton[]; code?: string; @@ -349,6 +351,7 @@ export interface IChatToolInvocation { progress: IObservable<{ message?: string | IMarkdownString; progress: number | undefined }>; readonly toolId: string; readonly toolCallId: string; + readonly fromSubAgent?: boolean; isCompletePromise: Promise; isComplete: boolean; @@ -380,6 +383,7 @@ export interface IChatToolInvocationSerialized { toolCallId: string; toolId: string; source: ToolDataSource; + readonly fromSubAgent?: boolean; kind: 'toolInvocationSerialized'; } @@ -639,6 +643,8 @@ export interface IChatEditorLocationData { document: URI; selection: ISelection; wholeRange: IRange; + close: () => void; + delegateSessionId: string | undefined; } export interface IChatNotebookLocationData { @@ -695,9 +701,11 @@ export interface IChatService { isPersistedSessionEmpty(sessionId: string): boolean; loadSessionFromContent(data: IExportableChatData | ISerializableChatData | URI): IChatModel | undefined; loadSessionForResource(resource: URI, location: ChatAgentLocation, token: CancellationToken): Promise; + readonly editingSessions: IChatEditingSession[]; + getChatSessionFromInternalId(sessionId: string): { chatSessionType: string; chatSessionId: string; isUntitled: boolean } | undefined; /** - * Returns whether the request was accepted. + * Returns whether the request was accepted.` */ sendRequest(sessionId: string, message: string, options?: IChatSendRequestOptions): Promise; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 994b6822f9c..43e571cd16a 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -14,6 +14,7 @@ import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposa import { revive } from '../../../../base/common/marshalling.js'; import { autorun, derived, IObservable, ObservableMap } from '../../../../base/common/observable.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; +import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; import { localize } from '../../../../nls.js'; @@ -72,6 +73,7 @@ export class ChatService extends Disposable implements IChatService { private readonly _sessionModels = new ObservableMap(); private readonly _contentProviderSessionModels = new Map>(); + private readonly _modelToExternalSession = new Map(); private readonly _pendingRequests = this._register(new DisposableMap()); private _persistedSessions: ISerializableChatsData; @@ -162,6 +164,10 @@ export class ChatService extends Disposable implements IChatService { }); } + public get editingSessions() { + return [...this._sessionModels.values()].map(v => v.editingSession).filter(isDefined); + } + isEnabled(location: ChatAgentLocation): boolean { return this.chatAgentService.getContributedDefaultAgent(location) !== undefined; } @@ -466,6 +472,8 @@ export class ChatService extends Disposable implements IChatService { const content = await this.chatSessionService.provideChatSessionContent(chatSessionType, parsed.sessionId, CancellationToken.None); const model = this._startSession(undefined, location, true, CancellationToken.None, chatSessionType); + // Record mapping from internal model session id to external contributed chat session identity + this._modelToExternalSession.set(model.sessionId, { chatSessionType, chatSessionId: parsed.sessionId }); if (!this._contentProviderSessionModels.has(chatSessionType)) { this._contentProviderSessionModels.set(chatSessionType, new Map()); } @@ -474,6 +482,7 @@ export class ChatService extends Disposable implements IChatService { disposables.add(model.onDidDispose(() => { this._contentProviderSessionModels?.get(chatSessionType)?.delete(parsed.sessionId); + this._modelToExternalSession.delete(model.sessionId); content.dispose(); })); @@ -569,6 +578,17 @@ export class ChatService extends Disposable implements IChatService { return model; } + getChatSessionFromInternalId(modelSessionId: string): { chatSessionType: string; chatSessionId: string; isUntitled: boolean } | undefined { + const data = this._modelToExternalSession.get(modelSessionId); + if (!data) { + return; + } + return { + ...data, + isUntitled: data.chatSessionId.startsWith('untitled-'), // TODO(jospicer) + }; + } + async resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise { const model = this._sessionModels.get(request.session.sessionId); if (!model && model !== request.session) { @@ -800,7 +820,7 @@ export class ChatService extends Disposable implements IChatService { rejectedConfirmationData: options?.rejectedConfirmationData, userSelectedModelId: options?.userSelectedModelId, userSelectedTools: options?.userSelectedTools?.get(), - modeInstructions: options?.modeInfo?.instructions, + modeInstructions: options?.modeInfo?.modeInstructions, editedFileEvents: request.editedFileEvents } satisfies IChatAgentRequest; }; diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index c2a3c1ad3f0..a7d599dae97 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -75,8 +75,6 @@ export interface IChatSessionItemProvider { provideChatSessionItems(token: CancellationToken): Promise; provideNewChatSessionItem?(options: { request: IChatAgentRequest; - prompt?: string; - history?: any[]; metadata?: any; }, token: CancellationToken): Promise; } @@ -99,8 +97,6 @@ export interface IChatSessionsService { getAllChatSessionItemProviders(): IChatSessionItemProvider[]; provideNewChatSessionItem(chatSessionType: string, options: { request: IChatAgentRequest; - prompt?: string; - history?: any[]; metadata?: any; }, token: CancellationToken): Promise; provideChatSessionItems(chatSessionType: string, token: CancellationToken): Promise; diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 956bac9d28e..e5b5b6ca3a7 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -18,7 +18,8 @@ export enum ChatConfiguration { ThinkingStyle = 'chat.agent.thinkingStyle', UseChatSessionsForCloudButton = 'chat.useChatSessionsForCloudButton', ShowAgentSessionsViewDescription = 'chat.showAgentSessionsViewDescription', - EmptyStateHistoryEnabled = 'chat.emptyState.history.enabled' + EmptyStateHistoryEnabled = 'chat.emptyState.history.enabled', + NotifyWindowOnResponseReceived = 'chat.notifyWindowOnResponseReceived', } /** diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index 17f8b3016fd..563bcd07829 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -124,6 +124,10 @@ export interface IToolInvocation { context: IToolInvocationContext | undefined; chatRequestId?: string; chatInteractionId?: string; + /** + * Lets us add some nicer UI to toolcalls that came from a sub-agent, but in the long run, this should probably just be rendered in a similar way to thinking text + tool call groups + */ + fromSubAgent?: boolean; toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent; modelId?: string; } @@ -186,6 +190,7 @@ export interface IToolResult { toolResultMessage?: string | IMarkdownString; toolResultDetails?: Array | IToolResultInputOutputDetails | IToolResultOutputDetails; toolResultError?: string; + toolMetadata?: unknown; } export function toolResultHasBuffers(result: IToolResult): boolean { @@ -323,14 +328,22 @@ export interface ILanguageModelToolsService { getToolAutoConfirmation(toolId: string): 'workspace' | 'profile' | 'session' | 'never'; resetToolAutoConfirmation(): void; cancelToolCallsForRequest(requestId: string): void; - toToolEnablementMap(toolOrToolSetNames: Set): Record; - toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap; - toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[]; readonly toolSets: IObservable>; getToolSet(id: string): ToolSet | undefined; getToolSetByName(name: string): ToolSet | undefined; createToolSet(source: ToolDataSource, id: string, referenceName: string, options?: { icon?: ThemeIcon; description?: string }): ToolSet & IDisposable; + + // tool names in prompt files handling ('qualified names') + + getQualifiedToolNames(): Iterable; + getToolByQualifiedName(qualifiedName: string): IToolData | ToolSet | undefined; + getQualifiedToolName(tool: IToolData, toolSet?: ToolSet): string; + getDeprecatedQualifiedToolNames(): Map; + + toToolAndToolSetEnablementMap(qualifiedToolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap; + toQualifiedToolNames(map: IToolAndToolSetEnablementMap): string[]; + toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[]; } export function createToolInputUri(toolOrId: IToolData | string): URI { diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index be046c0f7b3..5fd3e957184 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -177,6 +177,7 @@ export interface ILanguageModelChatMetadata { readonly vision?: boolean; readonly toolCalling?: boolean; readonly agentMode?: boolean; + readonly editTools?: ReadonlyArray; }; } @@ -298,9 +299,9 @@ export const languageModelChatProviderExtensionPoint = ExtensionsRegistry.regist } ] }, - activationEventsGenerator: (contribs: IUserFriendlyLanguageModel[], result: { push(item: string): void }) => { + activationEventsGenerator: function* (contribs: readonly IUserFriendlyLanguageModel[]) { for (const contrib of contribs) { - result.push(`onLanguageModelChatProvider:${contrib.vendor}`); + yield `onLanguageModelChatProvider:${contrib.vendor}`; } } }); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/asyncDecoder.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/asyncDecoder.ts deleted file mode 100644 index be10b50ebba..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/asyncDecoder.ts +++ /dev/null @@ -1,93 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from '../../../../../../../base/common/lifecycle.js'; -import { BaseDecoder } from './baseDecoder.js'; - -/** - * Asynchronous iterator wrapper for a decoder. - */ -export class AsyncDecoder, K extends NonNullable = NonNullable> extends Disposable { - // Buffer of messages that have been decoded but not yet consumed. - private readonly messages: T[] = []; - - /** - * A transient promise that is resolved when a new event - * is received. Used in the situation when there is no new - * data available and decoder stream did not finish yet, - * hence we need to wait until new event is received. - */ - private resolveOnNewEvent?: (value: void) => void; - - /** - * @param decoder The decoder instance to wrap. - * - * Note! Assumes ownership of the `decoder` object, hence will `dispose` - * it when the decoder stream is ended. - */ - constructor( - private readonly decoder: BaseDecoder, - ) { - super(); - - this._register(decoder); - } - - /** - * Async iterator implementation. - */ - async *[Symbol.asyncIterator](): AsyncIterator { - // callback is called when `data` or `end` event is received - const callback = (data?: T) => { - if (data !== undefined) { - this.messages.push(data); - } else { - this.decoder.removeListener('data', callback); - this.decoder.removeListener('end', callback); - } - - // is the promise resolve callback is present, - // then call it and remove the reference - if (this.resolveOnNewEvent) { - this.resolveOnNewEvent(); - delete this.resolveOnNewEvent; - } - }; - - /** - * !NOTE! The order of event subscriptions below is critical here because - * the `data` event is also starts the stream, hence changing - * the order of event subscriptions can lead to race conditions. - * See {@link ReadableStreamEvents} for more info. - */ - - this.decoder.on('end', callback); - this.decoder.on('data', callback); - - // start flowing the decoder stream - this.decoder.start(); - - while (true) { - const maybeMessage = this.messages.shift(); - if (maybeMessage !== undefined) { - yield maybeMessage; - continue; - } - - // if no data available and stream ended, we're done - if (this.decoder.ended) { - this.dispose(); - - return null; - } - - // stream isn't ended so wait for the new - // `data` or `end` event to be received - await new Promise((resolve) => { - this.resolveOnNewEvent = resolve; - }); - } - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/baseDecoder.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/baseDecoder.ts deleted file mode 100644 index 76d1745d3c6..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/baseDecoder.ts +++ /dev/null @@ -1,361 +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 { Emitter } from '../../../../../../../base/common/event.js'; -import { ReadableStream } from '../../../../../../../base/common/stream.js'; -import { DeferredPromise } from '../../../../../../../base/common/async.js'; -import { AsyncDecoder } from './asyncDecoder.js'; -import { assert, assertNever } from '../../../../../../../base/common/assert.js'; -import { DisposableMap, IDisposable } from '../../../../../../../base/common/lifecycle.js'; -import { ObservableDisposable } from '../../utils/observableDisposable.js'; - -/** - * Event names of {@link ReadableStream} stream. - */ -export type TStreamListenerNames = 'data' | 'error' | 'end'; - -/** - * Base decoder class that can be used to convert stream messages data type - * from one type to another. For instance, a stream of binary data can be - * "decoded" into a stream of well defined objects. - * Intended to be a part of "codec" implementation rather than used directly. - */ -export abstract class BaseDecoder< - T extends NonNullable, - K extends NonNullable = NonNullable, -> extends ObservableDisposable implements ReadableStream { - /** - * Private attribute to track if the stream has ended. - */ - private _ended = false; - - protected readonly _onData = this._register(new Emitter()); - private readonly _onEnd = this._register(new Emitter()); - private readonly _onError = this._register(new Emitter()); - - /** - * A store of currently registered event listeners. - */ - private readonly _listeners: DisposableMap< - TStreamListenerNames, - DisposableMap - > = this._register(new DisposableMap()); - - /** - * This method is called when a new incoming data - * is received from the input stream. - */ - protected abstract onStreamData(data: K): void; - - /** - * @param stream The input stream to decode. - */ - constructor( - protected readonly stream: ReadableStream, - ) { - super(); - } - - /** - * Private attribute to track if the stream has started. - */ - private started = false; - - /** - * Promise that resolves when the stream has ended, either by - * receiving the `end` event or by a disposal, but not when - * the `error` event is received alone. - * The promise is true if the stream has ended, and false - * if the stream has been disposed without ending. - */ - private settledPromise = new DeferredPromise(); - - /** - * Promise that resolves when the stream has ended, either by - * receiving the `end` event or by a disposal, but not when - * the `error` event is received alone. - * The promise is true if the stream has ended, and false - * if the stream has been disposed without ending. - * - * @throws If the stream was not yet started to prevent this - * promise to block the consumer calls indefinitely. - */ - public get settled(): Promise { - // if the stream has not started yet, the promise might - // block the consumer calls indefinitely if they forget - // to call the `start()` method, or if the call happens - // after await on the `settled` promise; to forbid this - // confusion, we require the stream to be started first - assert( - this.started, - [ - 'Cannot get `settled` promise of a stream that has not been started.', - 'Please call `start()` first.', - ].join(' '), - ); - - return this.settledPromise.p; - } - - /** - * Start receiving data from the stream. - * @throws if the decoder stream has already ended. - */ - public start(): this { - assert( - this._ended === false, - 'Cannot start stream that has already ended.', - ); - assert( - this.isDisposed === false, - 'Cannot start stream that has already disposed.', - ); - - // if already started, nothing to do - if (this.started) { - return this; - } - this.started = true; - - /** - * !NOTE! The order of event subscriptions is critical here because - * the `data` event is also starts the stream, hence changing - * the order of event subscriptions can lead to race conditions. - * See {@link ReadableStreamEvents} for more info. - */ - this.stream.on('end', this.onStreamEnd.bind(this)); - this.stream.on('error', this.onStreamError.bind(this)); - this.stream.on('data', this.tryOnStreamData.bind(this)); - - // this allows to compose decoders together, - if a decoder - // instance is passed as a readable stream to this decoder, - // then we need to call `start` on it too - if (this.stream instanceof BaseDecoder) { - this.stream.start(); - } - - return this; - } - - /** - * Check if the decoder has been ended hence has - * no more data to produce. - */ - public get ended(): boolean { - return this._ended; - } - - /** - * Automatically catch and dispatch errors thrown inside `onStreamData`. - */ - private tryOnStreamData(data: K): void { - try { - this.onStreamData(data); - } catch (error) { - this.onStreamError(error); - } - } - - 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: TStreamListenerNames, callback: unknown): void { - if (event === 'data') { - return this.onData(callback as (data: T) => void); - } - - if (event === 'error') { - return this.onError(callback as (error: Error) => void); - } - - if (event === 'end') { - return this.onEnd(callback as () => void); - } - - assertNever(event, `Invalid event name '${event}'`); - } - - /** - * Add listener for the `data` event. - * @throws if the decoder stream has already ended. - */ - public onData(callback: (data: T) => void): void { - assert( - !this.ended, - 'Cannot subscribe to the `data` event because the decoder stream has already ended.', - ); - - let currentListeners = this._listeners.get('data'); - - if (!currentListeners) { - currentListeners = new DisposableMap(); - this._listeners.set('data', currentListeners); - } - - currentListeners.set(callback, this._onData.event(callback)); - } - - /** - * Add listener for the `error` event. - * @throws if the decoder stream has already ended. - */ - public onError(callback: (error: Error) => void): void { - assert( - !this.ended, - 'Cannot subscribe to the `error` event because the decoder stream has already ended.', - ); - - let currentListeners = this._listeners.get('error'); - - if (!currentListeners) { - currentListeners = new DisposableMap(); - this._listeners.set('error', currentListeners); - } - - currentListeners.set(callback, this._onError.event(callback)); - } - - /** - * Add listener for the `end` event. - * @throws if the decoder stream has already ended. - */ - public onEnd(callback: () => void): void { - assert( - !this.ended, - 'Cannot subscribe to the `end` event because the decoder stream has already ended.', - ); - - let currentListeners = this._listeners.get('end'); - - if (!currentListeners) { - currentListeners = new DisposableMap(); - this._listeners.set('end', currentListeners); - } - - currentListeners.set(callback, this._onEnd.event(callback)); - } - - /** - * Pauses the stream. - */ - public pause(): void { - this.stream.pause(); - } - - /** - * Resumes the stream if it has been paused. - * @throws if the decoder stream has already ended. - */ - public resume(): void { - assert( - this.ended === false, - 'Cannot resume the stream because it has already ended.', - ); - - this.stream.resume(); - } - - /** - * Destroys(disposes) the stream. - */ - public destroy(): void { - this.dispose(); - } - - /** - * Removes a previously-registered event listener for a specified event. - * - * Note! - * - the callback function must be the same as the one that was used when - * registering the event listener as it is used as an identifier to - * remove the listener - * - this method is idempotent and results in no-op if the listener is - * not found, therefore passing incorrect `callback` function may - * result in silent unexpected behavior - */ - public removeListener(eventName: TStreamListenerNames, callback: Function): void { - const listeners = this._listeners.get(eventName); - if (listeners === undefined) { - return; - } - - for (const [listener] of listeners) { - if (listener !== callback) { - continue; - } - - listeners.deleteAndDispose(listener); - } - } - - /** - * This method is called when the input stream ends. - */ - protected onStreamEnd(): void { - if (this._ended) { - return; - } - - this._ended = true; - this._onEnd.fire(); - this.settledPromise.complete(this._ended); - } - - /** - * This method is called when the input stream emits an error. - * We re-emit the error here by default, but subclasses can - * override this method to handle the error differently. - */ - private onStreamError(error: Error): void { - this._onError.fire(error); - } - - /** - * Consume all messages from the stream, blocking until the stream finishes. - * @throws if the decoder stream has already ended. - */ - public async consumeAll(): Promise { - assert( - !this._ended, - 'Cannot consume all messages of the stream that has already ended.', - ); - - const messages = []; - - for await (const maybeMessage of this) { - if (maybeMessage === null) { - break; - } - - messages.push(maybeMessage); - } - - return messages; - } - - /** - * Async iterator interface for the decoder. - * @throws if the decoder stream has already ended. - */ - [Symbol.asyncIterator](): AsyncIterator { - assert( - !this._ended, - 'Cannot iterate on messages of the stream that has already ended.', - ); - - const asyncDecoder = this._register(new AsyncDecoder(this)); - - return asyncDecoder[Symbol.asyncIterator](); - } - - public override dispose(): void { - this.settledPromise.complete(this.ended); - - this._listeners.clearAndDisposeAll(); - this.stream.destroy(); - - super.dispose(); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/baseToken.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/baseToken.ts deleted file mode 100644 index ba81a78738e..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/baseToken.ts +++ /dev/null @@ -1,150 +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 { assert } from '../../../../../../../base/common/assert.js'; -import { IRange, Range } from '../../../../../../../editor/common/core/range.js'; - -/** - * Base class for all tokens with a `range` that reflects - * token position in the original text. - */ -export abstract class BaseToken { - constructor( - private tokenRange: Range, - ) { } - - /** - * Range of the token in the original text. - */ - public get range(): Range { - return this.tokenRange; - } - - /** - * Text representation of the token. - */ - public abstract get text(): TText; - - /** - * Check if this token has the same range as another one. - */ - public sameRange(other: Range): boolean { - return this.range.equalsRange(other); - } - - /** - * Returns a string representation of the token. - */ - public abstract toString(): string; - - /** - * Check if this token is equal to another one. - */ - public equals(other: BaseToken): other is typeof this { - if (other.constructor !== this.constructor) { - return false; - } - - if (this.text.length !== other.text.length) { - return false; - } - - if (this.text !== other.text) { - return false; - } - - return this.sameRange(other.range); - } - - /** - * Change `range` of the token with provided range components. - */ - public withRange(components: Partial): this { - this.tokenRange = new Range( - components.startLineNumber ?? this.range.startLineNumber, - components.startColumn ?? this.range.startColumn, - components.endLineNumber ?? this.range.endLineNumber, - components.endColumn ?? this.range.endColumn, - ); - - return this; - } - - /** - * Collapse range of the token to its start position. - * See {@link Range.collapseToStart} for more details. - */ - public collapseRangeToStart(): this { - this.tokenRange = this.tokenRange.collapseToStart(); - - return this; - } - - /** - * Render a list of tokens into a string. - */ - public static render( - tokens: readonly BaseToken[], - delimiter: string = '', - ): string { - return tokens.map(token => token.text).join(delimiter); - } - - /** - * Returns the full range of a list of tokens in which the first token is - * used as the start of a tokens sequence and the last token reflects the end. - * - * @throws if: - * - provided {@link tokens} list is empty - * - the first token start number is greater than the start line of the last token - * - if the first and last token are on the same line, the first token start column must - * be smaller than the start column of the last token - */ - public static fullRange(tokens: readonly BaseToken[]): Range { - assert( - tokens.length > 0, - 'Cannot get full range for an empty list of tokens.', - ); - - const firstToken = tokens[0]; - const lastToken = tokens[tokens.length - 1]; - - // sanity checks for the full range we would construct - assert( - firstToken.range.startLineNumber <= lastToken.range.startLineNumber, - 'First token must start on previous or the same line as the last token.', - ); - - if ((firstToken !== lastToken) && (firstToken.range.startLineNumber === lastToken.range.startLineNumber)) { - assert( - firstToken.range.endColumn <= lastToken.range.startColumn, - [ - 'First token must end at least on previous or the same column as the last token.', - `First token: ${firstToken}; Last token: ${lastToken}.`, - ].join('\n'), - ); - } - - return new Range( - firstToken.range.startLineNumber, - firstToken.range.startColumn, - lastToken.range.endLineNumber, - lastToken.range.endColumn, - ); - } - - /** - * Shorten version of the {@link text} property. - */ - public shortText( - maxLength: number = 32, - ): string { - if (this.text.length <= maxLength) { - return this.text; - } - - return `${this.text.slice(0, maxLength - 1)}...`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/compositeToken.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/compositeToken.ts deleted file mode 100644 index e4eb1470a03..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/compositeToken.ts +++ /dev/null @@ -1,63 +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 { BaseToken } from './baseToken.js'; - -/** - * Composite token consists of a list of other tokens. - * Composite token consists of a list of other tokens. - */ -export abstract class CompositeToken< - TTokens extends readonly BaseToken[], -> extends BaseToken { - /** - * Reference to the list of child tokens. - */ - protected readonly childTokens: [...TTokens]; - - constructor( - tokens: TTokens, - ) { - super(BaseToken.fullRange(tokens)); - - this.childTokens = [...tokens]; - } - - public override get text(): string { - return BaseToken.render(this.childTokens); - } - - /** - * Tokens that this composite token consists of. - */ - public get children(): TTokens { - return this.childTokens; - } - - /** - * Check if this token is equal to another one, - * including all of its child tokens. - */ - public override equals(other: BaseToken): other is typeof this { - if (super.equals(other) === false) { - return false; - } - - if (this.children.length !== other.children.length) { - return false; - } - - for (let i = 0; i < this.children.length; i++) { - const childToken = this.children[i]; - const otherChildToken = other.children[i]; - - if (childToken.equals(otherChildToken) === false) { - return false; - } - } - - return true; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/constants.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/constants.ts deleted file mode 100644 index 779e53a1ef1..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/constants.ts +++ /dev/null @@ -1,16 +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 { NewLine } from '../linesCodec/tokens/newLine.js'; -import { CarriageReturn } from '../linesCodec/tokens/carriageReturn.js'; -import { FormFeed, SpacingToken } from '../simpleCodec/tokens/tokens.js'; - -/** - * List of valid "space" tokens that are valid between different - * records of a Front Matter header. - */ -export const VALID_INTER_RECORD_SPACING_TOKENS = Object.freeze([ - SpacingToken, CarriageReturn, NewLine, FormFeed, -]); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/frontMatterDecoder.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/frontMatterDecoder.ts deleted file mode 100644 index 704e10f1754..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/frontMatterDecoder.ts +++ /dev/null @@ -1,156 +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 { Word } from '../simpleCodec/tokens/tokens.js'; -import { assert } from '../../../../../../../../base/common/assert.js'; -import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; -import { VALID_INTER_RECORD_SPACING_TOKENS } from './constants.js'; -import { ReadableStream } from '../../../../../../../../base/common/stream.js'; -import { FrontMatterToken, FrontMatterRecord } from './tokens/index.js'; -import { BaseDecoder } from '../baseDecoder.js'; -import { SimpleDecoder, type TSimpleDecoderToken } from '../simpleCodec/simpleDecoder.js'; -import { ObjectStream } from '../utils/objectStream.js'; -import { PartialFrontMatterRecordNameWithDelimiter } from './parsers/frontMatterRecord/frontMatterRecordNameWithDelimiter.js'; -import { PartialFrontMatterRecord } from './parsers/frontMatterRecord/frontMatterRecord.js'; -import { PartialFrontMatterRecordName } from './parsers/frontMatterRecord/frontMatterRecordName.js'; -import { FrontMatterParserFactory } from './parsers/frontMatterParserFactory.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; - - private readonly parserFactory: FrontMatterParserFactory; - - constructor( - stream: ReadableStream | ObjectStream, - ) { - if (stream instanceof ObjectStream) { - super(stream); - } else { - super(new SimpleDecoder(stream)); - } - this.parserFactory = new FrontMatterParserFactory(); - } - - 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) { - // front matter record token is the spacial case - because it can - // contain trailing space tokens, we want to emit "trimmed" record - // token and the trailing spaces tokens separately - const trimmedTokens = (nextParser instanceof FrontMatterRecord) - ? nextParser.trimValueEnd() - : []; - - this._onData.fire(nextParser); - - // re-emit all trailing space tokens if present - for (const trimmedToken of trimmedTokens) { - this._onData.fire(trimmedToken); - } - - 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 = this.parserFactory.createRecordName(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_INTER_RECORD_SPACING_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; - } - - assert( - this.current instanceof PartialFrontMatterRecord, - 'Only partial front matter records can be processed on stream end.', - ); - - const record = this.current.asRecordToken(); - const trimmedTokens = record.trimValueEnd(); - - this._onData.fire(record); - - for (const trimmedToken of trimmedTokens) { - this._onData.fire(trimmedToken); - } - } catch (_error) { - 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/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterArray.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterArray.ts deleted file mode 100644 index df84fa012c7..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterArray.ts +++ /dev/null @@ -1,197 +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 { assert } from '../../../../../../../../../base/common/assert.js'; -import { type PartialFrontMatterValue } from './frontMatterValue.js'; -import { FrontMatterArray } from '../tokens/frontMatterArray.js'; -import { assertDefined } from '../../../../../../../../../base/common/types.js'; -import { VALID_INTER_RECORD_SPACING_TOKENS } from '../constants.js'; -import { FrontMatterValueToken } from '../tokens/frontMatterToken.js'; -import { FrontMatterSequence } from '../tokens/frontMatterSequence.js'; -import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; -import { Comma, LeftBracket, RightBracket } from '../../simpleCodec/tokens/tokens.js'; -import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; -import { type FrontMatterParserFactory } from './frontMatterParserFactory.js'; - -/** - * List of tokens that can go in-between array items - * and array brackets. - */ -const VALID_DELIMITER_TOKENS = Object.freeze([ - ...VALID_INTER_RECORD_SPACING_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 factory: FrontMatterParserFactory, - private readonly startToken: LeftBracket, - ) { - 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; - - // if token was not consume, call the `accept()` method - // recursively so that the current parser can re-process - // the token (e.g., a comma or a closing square bracket) - if (wasTokenConsumed === false) { - return this.accept(token); - } - - 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, - }; - } - } - - // is an array item value is allowed at this position, create a new - // value parser and start the value parsing process using it - if (this.arrayItemAllowed === true) { - this.currentValueParser = this.factory.createValue( - (currentToken) => { - // comma or a closing square bracket must stop the parsing - // process of the value represented by a generic sequence of tokens - return ( - (currentToken instanceof RightBracket) - || (currentToken instanceof Comma) - ); - }, - ); - 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 { - 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) === false) { - continue; - } - - // the generic sequence tokens can have trailing spacing tokens, - // hence trim them to ensure the array contains only "clean" values - if (currentToken instanceof FrontMatterSequence) { - currentToken.trimEnd(); - } - - valueTokens.push(currentToken); - } - - this.isConsumed = true; - return new FrontMatterArray([ - this.startToken, - ...valueTokens, - endToken, - ]); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterParserFactory.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterParserFactory.ts deleted file mode 100644 index 0be65371449..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterParserFactory.ts +++ /dev/null @@ -1,41 +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 { BaseToken } from '../../baseToken.js'; -import { LeftBracket } from '../../simpleCodec/tokens/brackets.js'; -import { Word } from '../../simpleCodec/tokens/word.js'; -import { FrontMatterRecordDelimiter, FrontMatterRecordName } from '../tokens/frontMatterRecord.js'; -import { TQuoteToken } from '../tokens/frontMatterString.js'; -import { PartialFrontMatterArray } from './frontMatterArray.js'; -import { PartialFrontMatterRecord } from './frontMatterRecord/frontMatterRecord.js'; -import { PartialFrontMatterRecordName } from './frontMatterRecord/frontMatterRecordName.js'; -import { PartialFrontMatterRecordNameWithDelimiter, TNameStopToken } from './frontMatterRecord/frontMatterRecordNameWithDelimiter.js'; -import { PartialFrontMatterSequence } from './frontMatterSequence.js'; -import { PartialFrontMatterString } from './frontMatterString.js'; -import { PartialFrontMatterValue } from './frontMatterValue.js'; - -export class FrontMatterParserFactory { - createRecord(tokens: [FrontMatterRecordName, FrontMatterRecordDelimiter]): PartialFrontMatterRecord { - return new PartialFrontMatterRecord(this, tokens); - } - createRecordName(startToken: Word): PartialFrontMatterRecordName { - return new PartialFrontMatterRecordName(this, startToken); - } - createRecordNameWithDelimiter(tokens: readonly [FrontMatterRecordName, TNameStopToken]): PartialFrontMatterRecordNameWithDelimiter { - return new PartialFrontMatterRecordNameWithDelimiter(this, tokens); - } - createArray(startToken: LeftBracket) { - return new PartialFrontMatterArray(this, startToken); - } - createValue(shouldStop: (token: BaseToken) => boolean): PartialFrontMatterValue { - return new PartialFrontMatterValue(this, shouldStop); - } - createString(startToken: TQuoteToken): PartialFrontMatterString { - return new PartialFrontMatterString(startToken); - } - createSequence(shouldStop: (token: BaseToken) => boolean): PartialFrontMatterSequence { - return new PartialFrontMatterSequence(shouldStop); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterRecord/frontMatterRecord.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterRecord/frontMatterRecord.ts deleted file mode 100644 index e4afee055b5..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterRecord/frontMatterRecord.ts +++ /dev/null @@ -1,210 +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 { BaseToken } from '../../../baseToken.js'; -import { NewLine } from '../../../linesCodec/tokens/newLine.js'; -import { PartialFrontMatterValue } from '../frontMatterValue.js'; -import { assertNever } from '../../../../../../../../../../base/common/assert.js'; -import { assertDefined } from '../../../../../../../../../../base/common/types.js'; -import { PartialFrontMatterSequence } from '../frontMatterSequence.js'; -import { CarriageReturn } from '../../../linesCodec/tokens/carriageReturn.js'; -import { type TSimpleDecoderToken } from '../../../simpleCodec/simpleDecoder.js'; -import { Word, FormFeed, SpacingToken } from '../../../simpleCodec/tokens/tokens.js'; -import { assertNotConsumed, ParserBase, type TAcceptTokenResult } from '../../../simpleCodec/parserBase.js'; -import { FrontMatterValueToken, FrontMatterRecordName, FrontMatterRecordDelimiter, FrontMatterRecord } from '../../tokens/index.js'; -import { type FrontMatterParserFactory } from '../frontMatterParserFactory.js'; - -/** - * Type of a next parser that can be returned by {@link PartialFrontMatterRecord}. - */ -type TNextParser = PartialFrontMatterRecord | FrontMatterRecord; - -/** - * 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 { - /** - * Token that represents the 'name' part of the record. - */ - private readonly recordNameToken: FrontMatterRecordName; - - /** - * Token that represents the 'delimiter' part of the record. - */ - private readonly recordDelimiterToken: FrontMatterRecordDelimiter; - - constructor( - private readonly factory: FrontMatterParserFactory, - tokens: [FrontMatterRecordName, FrontMatterRecordDelimiter], - ) { - super(tokens); - this.recordNameToken = tokens[0]; - this.recordDelimiterToken = tokens[1]; - } - - /** - * Current parser reference responsible for parsing the "value" part of the record. - */ - private valueParser?: PartialFrontMatterValue | PartialFrontMatterSequence; - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - if (this.valueParser !== undefined) { - const acceptResult = this.valueParser.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.valueParser; - - this.isConsumed = true; - try { - return { - result: 'success', - nextParser: new FrontMatterRecord([ - this.recordNameToken, - this.recordDelimiterToken, - nextParser, - ]), - wasTokenConsumed, - }; - } catch (_error) { - return { - result: 'failure', - wasTokenConsumed, - }; - } - } - - this.valueParser = nextParser; - return { - result: 'success', - nextParser: this, - wasTokenConsumed, - }; - } - - // iterate until the first non-space token is found - if (token instanceof SpacingToken) { - 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.valueParser = this.factory.createValue(shouldEndTokenSequence); - - return this.accept(token); - } - - // in all other cases, collect all the subsequent tokens into - // a "sequence of tokens" until a new line is found - this.valueParser = this.factory.createSequence( - shouldEndTokenSequence, - ); - - // if we reached this "generic sequence" parser point, but the current token is - // already of a type that stops such sequence, we must have accumulated some - // spacing tokens, hence pass those to the parser and end the sequence immediately - if (shouldEndTokenSequence(token)) { - const spaceTokens = this.currentTokens - .slice(this.startTokensCount); - - // if no space tokens accumulated at all, create an "empty" one this is needed - // to ensure that the parser always has at least one token hence it can have - // a valid range and can be interpreted as a real "value" token of the record - if (spaceTokens.length === 0) { - spaceTokens.push( - Word.newOnLine( - '', - token.range.startLineNumber, - token.range.startColumn, - ), - ); - } - - this.valueParser.addTokens(spaceTokens); - - return { - result: 'success', - nextParser: this.asRecordToken(), - wasTokenConsumed: false, - }; - } - - // otherwise use the "generic sequence" parser moving on - return this.accept(token); - } - - /** - * Convert current parser into a {@link FrontMatterRecord} token. - * - * @throws if no current parser is present, or it is not of the {@link PartialFrontMatterValue} - * or {@link PartialFrontMatterSequence} types - */ - public asRecordToken(): FrontMatterRecord { - assertDefined( - this.valueParser, - 'Current value parser must be defined.' - ); - - if ( - (this.valueParser instanceof PartialFrontMatterValue) - || (this.valueParser instanceof PartialFrontMatterSequence) - ) { - const valueToken = this.valueParser.asSequenceToken(); - this.currentTokens.push(valueToken); - - this.isConsumed = true; - return new FrontMatterRecord([ - this.recordNameToken, - this.recordDelimiterToken, - valueToken, - ]); - } - - assertNever( - this.valueParser, - `Unexpected value parser '${this.valueParser}'.`, - ); - } -} - -/** - * Callback to check if a current token should end a - * record value that is a generic sequence of tokens. - */ -function shouldEndTokenSequence(token: BaseToken): token is (NewLine | CarriageReturn | FormFeed) { - return ( - (token instanceof NewLine) - || (token instanceof CarriageReturn) - || (token instanceof FormFeed) - ); -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterRecord/frontMatterRecordName.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterRecord/frontMatterRecordName.ts deleted file mode 100644 index 84c94b75211..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterRecord/frontMatterRecordName.ts +++ /dev/null @@ -1,74 +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 { type TSimpleDecoderToken } from '../../../simpleCodec/simpleDecoder.js'; -import { FrontMatterRecordName, type TRecordNameToken } from '../../tokens/index.js'; -import { Colon, Word, Dash, SpacingToken } from '../../../simpleCodec/tokens/tokens.js'; -import { type PartialFrontMatterRecordNameWithDelimiter } from './frontMatterRecordNameWithDelimiter.js'; -import { assertNotConsumed, ParserBase, type TAcceptTokenResult } from '../../../simpleCodec/parserBase.js'; -import { type FrontMatterParserFactory } from '../frontMatterParserFactory.js'; - -/** - * Tokens that can be used inside a record name. - */ -const VALID_NAME_TOKENS = [Word, Dash]; - -/** - * Type of a next parser that can be returned by {@link PartialFrontMatterRecordName}. - */ -type TNextParser = PartialFrontMatterRecordName | PartialFrontMatterRecordNameWithDelimiter; - -/** - * 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( - private readonly factory: FrontMatterParserFactory, - 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 - if ((token instanceof Colon) || (token instanceof SpacingToken)) { - const recordName = new FrontMatterRecordName(this.currentTokens); - - this.isConsumed = true; - return { - result: 'success', - nextParser: this.factory.createRecordNameWithDelimiter([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, - }; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterRecord/frontMatterRecordNameWithDelimiter.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterRecord/frontMatterRecordNameWithDelimiter.ts deleted file mode 100644 index 59735860631..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterRecord/frontMatterRecordNameWithDelimiter.ts +++ /dev/null @@ -1,105 +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 { assert } from '../../../../../../../../../../base/common/assert.js'; -import { type PartialFrontMatterRecord } from './frontMatterRecord.js'; -import { Colon, SpacingToken } from '../../../simpleCodec/tokens/tokens.js'; -import { type TSimpleDecoderToken } from '../../../simpleCodec/simpleDecoder.js'; -import { FrontMatterRecordName, FrontMatterRecordDelimiter } from '../../tokens/index.js'; -import { assertNotConsumed, ParserBase, type TAcceptTokenResult } from '../../../simpleCodec/parserBase.js'; -import { type FrontMatterParserFactory } from '../frontMatterParserFactory.js'; - -/** - * Type for tokens that stop a front matter record name sequence. - */ -export type TNameStopToken = Colon | SpacingToken; - -/** - * Type for the next parser that can be returned by {@link PartialFrontMatterRecordNameWithDelimiter}. - */ -type TNextParser = PartialFrontMatterRecordNameWithDelimiter | PartialFrontMatterRecord; - -/** - * Parser for a record `name` with the `: ` delimiter. - * - * * E.g., `name:` in the example below: - * - * ``` - * name: 'value' - * ``` - */ -export class PartialFrontMatterRecordNameWithDelimiter extends ParserBase< - FrontMatterRecordName | TNameStopToken, - TNextParser -> { - constructor( - private readonly factory: FrontMatterParserFactory, - tokens: readonly [FrontMatterRecordName, TNameStopToken], - ) { - super([...tokens]); - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - const previousToken = this.currentTokens[this.currentTokens.length - 1]; - const isSpacingToken = (token instanceof SpacingToken); - - // delimiter must always be a `:` followed by a "space" character - // once we encounter that sequence, we can transition to the next parser - if (isSpacingToken && (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: this.factory.createRecord( - [recordName, recordDelimiter], - ), - wasTokenConsumed: true, - }; - } - - // allow some spacing before the colon delimiter - if (token instanceof SpacingToken) { - 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, - }; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterSequence.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterSequence.ts deleted file mode 100644 index 0d91c32528c..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterSequence.ts +++ /dev/null @@ -1,78 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { BaseToken } from '../../baseToken.js'; -import { FrontMatterSequence } from '../tokens/frontMatterSequence.js'; -import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; -import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; - -/** - * Parser responsible for parsing a "generic sequence of tokens" - * of an arbitrary length in a Front Matter header. - */ -export class PartialFrontMatterSequence extends ParserBase< - TSimpleDecoderToken, - PartialFrontMatterSequence | FrontMatterSequence -> { - constructor( - /** - * Callback function that is called to check if the current token - * should stop the parsing process of the current generic "value" - * sequence of arbitrary tokens by returning `true`. - * - * When this happens, the parser *will not consume* the token that - * was passed to the `shouldStop` callback or to its `accept` method. - * On the other hand, the parser will be "consumed" hence using it - * to process other tokens will yield an error. - */ - private readonly shouldStop: (token: BaseToken) => boolean, - ) { - super([]); - } - - @assertNotConsumed - public accept( - token: TSimpleDecoderToken, - ): TAcceptTokenResult { - - // collect all tokens until an end of the sequence is found - if (this.shouldStop(token)) { - this.isConsumed = true; - - return { - result: 'success', - nextParser: this.asSequenceToken(), - wasTokenConsumed: false, - }; - } - - this.currentTokens.push(token); - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } - - /** - * Add provided tokens to the list of the current parsed tokens. - */ - public addTokens( - tokens: readonly TSimpleDecoderToken[], - ): this { - this.currentTokens.push(...tokens); - - return this; - } - - /** - * Convert the current parser into a {@link FrontMatterSequence} token. - */ - public asSequenceToken(): FrontMatterSequence { - this.isConsumed = true; - - return new FrontMatterSequence(this.currentTokens); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterString.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterString.ts deleted file mode 100644 index d0ed92746d6..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterString.ts +++ /dev/null @@ -1,69 +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 { assert } from '../../../../../../../../../base/common/assert.js'; -import { SimpleToken } from '../../simpleCodec/tokens/tokens.js'; -import { assertDefined } from '../../../../../../../../../base/common/types.js'; -import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; -import { FrontMatterString, type TQuoteToken } from '../tokens/frontMatterString.js'; -import { assertNotConsumed, ParserBase, type 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/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterValue.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterValue.ts deleted file mode 100644 index 710767aed40..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/parsers/frontMatterValue.ts +++ /dev/null @@ -1,184 +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 { BaseToken } from '../../baseToken.js'; -import { type PartialFrontMatterArray } from './frontMatterArray.js'; -import { type PartialFrontMatterString } from './frontMatterString.js'; -import { asBoolean, FrontMatterBoolean } from '../tokens/frontMatterBoolean.js'; -import { FrontMatterValueToken } from '../tokens/frontMatterToken.js'; -import { PartialFrontMatterSequence } from './frontMatterSequence.js'; -import { FrontMatterSequence } from '../tokens/frontMatterSequence.js'; -import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; -import { Word, Quote, DoubleQuote, LeftBracket } from '../../simpleCodec/tokens/tokens.js'; -import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; -import { type FrontMatterParserFactory } from './frontMatterParserFactory.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([ - 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 | PartialFrontMatterSequence; - - /** - * Get the tokens that were accumulated so far. - */ - public override get tokens(): readonly TSimpleDecoderToken[] { - if (this.currentValueParser === undefined) { - return []; - } - - return this.currentValueParser.tokens; - } - - constructor( - private readonly factory: FrontMatterParserFactory, - /** - * Callback function to pass to the {@link PartialFrontMatterSequence} - * if the current "value" sequence is not of a specific type. - */ - private readonly shouldStop: (token: BaseToken) => boolean, - ) { - super(); - } - - @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 = this.factory.createString(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 = this.factory.createArray(token); - - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } - - // if the first token represents a `word` try to parse a boolean - const maybeBoolean = FrontMatterBoolean.tryFromToken(token); - if (maybeBoolean !== null) { - this.isConsumed = true; - - return { - result: 'success', - nextParser: maybeBoolean, - wasTokenConsumed: true, - }; - } - - // in all other cases, collect all the subsequent tokens into - // a generic sequence of tokens until stopped by the `this.shouldStop` - // callback or the call to the 'this.asSequenceToken' method - this.currentValueParser = this.factory.createSequence(this.shouldStop); - - return this.accept(token); - } - - /** - * 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 | Word<'true' | 'false'> { - for (const ValidToken of VALID_VALUE_START_TOKENS) { - if (token instanceof ValidToken) { - return true; - } - } - - if ((token instanceof Word) && (asBoolean(token) !== null)) { - return true; - } - - return false; - } - - /** - * Check if the current 'value' sequence does not have a specific type - * and is represented by a generic sequence of tokens ({@link PartialFrontMatterSequence}). - */ - public get isSequence(): boolean { - if (this.currentValueParser === undefined) { - return false; - } - - return (this.currentValueParser instanceof PartialFrontMatterSequence); - } - - /** - * Convert current parser into a generic sequence of tokens. - */ - public asSequenceToken(): FrontMatterSequence { - this.isConsumed = true; - - return new FrontMatterSequence(this.tokens); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterArray.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterArray.ts deleted file mode 100644 index b8b01e42c79..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterArray.ts +++ /dev/null @@ -1,43 +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 { BaseToken } from '../../baseToken.js'; -import { LeftBracket, RightBracket } from '../../simpleCodec/tokens/tokens.js'; -import { FrontMatterValueToken, type TValueTypeName } from './frontMatterToken.js'; - -/** - * Token that represents an `array` value in a Front Matter header. - */ -export class FrontMatterArray extends FrontMatterValueToken<'array', [ - LeftBracket, - ...FrontMatterValueToken[], - RightBracket, -]> { - /** - * Name of the `array` value type. - */ - public override readonly valueTypeName = 'array'; - - /** - * List of the array items. - */ - public get items(): readonly FrontMatterValueToken[] { - const result = []; - - for (const token of this.children) { - if (token instanceof FrontMatterValueToken) { - result.push(token); - } - } - - return result; - } - - public override toString(): string { - const itemsString = BaseToken.render(this.items, ', '); - - return `front-matter-array(${itemsString})${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterBoolean.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterBoolean.ts deleted file mode 100644 index 3eae6d114ed..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterBoolean.ts +++ /dev/null @@ -1,88 +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 { BaseToken } from '../../baseToken.js'; -import { Word } from '../../simpleCodec/tokens/tokens.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', readonly [Word]> { - /** - * Name of the `boolean` value type. - */ - public override readonly valueTypeName = 'boolean'; - - /** - * Value of the `boolean` token. - */ - public readonly value: boolean; - - /** - * @throws if provided {@link Word} cannot be converted to a `boolean` value. - */ - constructor(token: Word) { - const value = asBoolean(token); - assertDefined( - value, - `Cannot convert '${token}' to a boolean value.`, - ); - - super([token]); - - this.value = value; - } - - /** - * Try creating a {@link FrontMatterBoolean} out of provided token. - * Unlike the constructor, this method does not throw, returning - * a 'null' value on failure instead. - */ - public static tryFromToken( - token: BaseToken, - ): FrontMatterBoolean | null { - if (token instanceof Word === false) { - return null; - } - - try { - return new FrontMatterBoolean(token); - } catch (_error) { - // noop - return null; - } - } - - public override equals(other: BaseToken): other is typeof this { - if (super.equals(other) === false) { - return false; - } - - return this.value === other.value; - } - - public override toString(): string { - return `front-matter-boolean(${this.shortText()})${this.range}`; - } -} - -/** - * Try to convert a {@link Word} token to a `boolean` value. - */ -export function 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/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterRecord.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterRecord.ts deleted file mode 100644 index dc498c6d667..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterRecord.ts +++ /dev/null @@ -1,118 +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 { BaseToken } from '../../baseToken.js'; -import { FrontMatterSequence } from './frontMatterSequence.js'; -import { Colon, Word, Dash, SpacingToken } from '../../simpleCodec/tokens/tokens.js'; -import { FrontMatterToken, FrontMatterValueToken, type TValueTypeName } from './frontMatterToken.js'; - -/** - * Type for tokens that can be used inside a record name. - */ -export type TNameToken = Word | Dash; - -/** - * Token representing a `record name` inside a Front Matter record. - * - * E.g., `name` in the example below: - * - * ``` - * --- - * name: 'value' - * --- - * ``` - */ -export class FrontMatterRecordName extends FrontMatterToken { - 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 { - 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< - readonly [FrontMatterRecordName, FrontMatterRecordDelimiter, FrontMatterValueToken] -> { - /** - * Token that represent `name` of the record. - * - * E.g., `tools` in the example below: - * - * ``` - * --- - * tools: ['value'] - * --- - * ``` - */ - public get nameToken(): FrontMatterRecordName { - return this.children[0]; - } - - /** - * Token that represent `value` of the record. - * - * E.g., `['value']` in the example below: - * - * ``` - * --- - * tools: ['value'] - * --- - * ``` - */ - public get valueToken(): FrontMatterValueToken { - return this.children[2]; - } - - /** - * Trim spacing tokens at the end of the record. - */ - public trimValueEnd(): readonly SpacingToken[] { - const { valueToken } = this; - - // only the "generic sequence" value tokens can hold - // some spacing tokens at the end of them - if ((valueToken instanceof FrontMatterSequence) === false) { - return []; - } - - const trimmedTokens = valueToken.trimEnd(); - // update the current range to reflect the current trimmed value - this.withRange( - BaseToken.fullRange(this.children), - ); - - return trimmedTokens; - } - - public override toString(): string { - return `front-matter-record(${this.shortText()})${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterSequence.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterSequence.ts deleted file mode 100644 index dcb6f70d5a3..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterSequence.ts +++ /dev/null @@ -1,79 +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 { BaseToken } from '../../baseToken.js'; -import { FrontMatterValueToken } from './frontMatterToken.js'; -import { Word, SpacingToken } from '../../simpleCodec/tokens/tokens.js'; -import { type TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; - - -/** - * Token represents a generic sequence of tokens in a Front Matter header. - */ -export class FrontMatterSequence extends FrontMatterValueToken { - /** - * @override Because this token represent a generic sequence of tokens, - * the type name is represented by the sequence of tokens itself - */ - public override get valueTypeName(): this { - return this; - } - - /** - * Text of the sequence value. The method exists to provide a - * consistent interface with {@link FrontMatterString} token. - * - * Note! that this method does not automatically trim spacing tokens - * in the sequence. If you need to get a trimmed value, call - * {@link trimEnd} method first. - */ - public get cleanText(): string { - return this.text; - } - - /** - * Trim spacing tokens at the end of the sequence. - */ - public trimEnd(): readonly SpacingToken[] { - const trimmedTokens = []; - - // iterate the tokens list from the end to the start, collecting - // all the spacing tokens we encounter until we reach a non-spacing token - let lastNonSpace = this.childTokens.length - 1; - while (lastNonSpace >= 0) { - const token = this.childTokens[lastNonSpace]; - - if (token instanceof SpacingToken) { - trimmedTokens.push(token); - lastNonSpace--; - - continue; - } - - break; - } - this.childTokens.length = lastNonSpace + 1; - - // if there are only spacing tokens were present add a single - // empty token to the sequence, so it has something to work with - if (this.childTokens.length === 0) { - this.collapseRangeToStart(); - this.childTokens.push(new Word(this.range, '')); - } - - // update the current range to reflect the current trimmed value - this.withRange( - BaseToken.fullRange(this.childTokens), - ); - - // trimmed tokens are collected starting from the end, - // moving to the start, hence reverse them before returning - return trimmedTokens.reverse(); - } - - public override toString(): string { - return `front-matter-sequence(${this.shortText()})${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterString.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterString.ts deleted file mode 100644 index 8a7283d1098..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterString.ts +++ /dev/null @@ -1,39 +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 { BaseToken } from '../../baseToken.js'; -import { FrontMatterValueToken } from './frontMatterToken.js'; -import { Quote, DoubleQuote } from '../../simpleCodec/tokens/tokens.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< - 'quoted-string', - readonly [TQuote, ...BaseToken[], TQuote] -> { - /** - * Name of the `string` value type. - */ - public override readonly valueTypeName = 'quoted-string'; - - /** - * Text of the string value without the wrapping quotes. - */ - public get cleanText(): string { - return BaseToken.render( - this.children.slice(1, this.children.length - 1), - ); - } - - public override toString(): string { - return `front-matter-string(${this.shortText()})${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterToken.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterToken.ts deleted file mode 100644 index d85f85230d7..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterToken.ts +++ /dev/null @@ -1,33 +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 { BaseToken } from '../../baseToken.js'; -import { CompositeToken } from '../../compositeToken.js'; -import { FrontMatterSequence } from './frontMatterSequence.js'; - -/** - * Base class for all tokens inside a Front Matter header. - */ -export abstract class FrontMatterToken< - TTokens extends readonly BaseToken[] = readonly BaseToken[], -> extends CompositeToken { } - -/** - * List of all currently supported value types. - */ -export type TValueTypeName = 'quoted-string' | 'boolean' | 'array' | FrontMatterSequence; - -/** - * Base class for all tokens that represent a `value` inside a Front Matter header. - */ -export abstract class FrontMatterValueToken< - TTypeName extends TValueTypeName = TValueTypeName, - TTokens extends readonly BaseToken[] = readonly BaseToken[], -> extends FrontMatterToken { - /** - * Type name of the `value` represented by this token. - */ - public abstract readonly valueTypeName: TTypeName; -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/index.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/index.ts deleted file mode 100644 index 033a1250fa5..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/frontMatterCodec/tokens/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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, -} from './frontMatterRecord.js'; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/linesDecoder.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/linesDecoder.ts deleted file mode 100644 index 5d9ad249d6f..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/linesDecoder.ts +++ /dev/null @@ -1,237 +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 { Line } from './tokens/line.js'; -import { Range } from '../../../../../../../../editor/common/core/range.js'; -import { NewLine } from './tokens/newLine.js'; -import { assert } from '../../../../../../../../base/common/assert.js'; -import { CarriageReturn } from './tokens/carriageReturn.js'; -import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; -import { assertDefined } from '../../../../../../../../base/common/types.js'; -import { BaseDecoder } from '../baseDecoder.js'; - -/** - * Any line break token type. - */ -export type TLineBreakToken = CarriageReturn | NewLine; - -/** - * Tokens produced by the {@link LinesDecoder}. - */ -export type TLineToken = Line | TLineBreakToken; - -/** - * The `decoder` part of the `LinesCodec` and is able to transform - * data from a binary stream into a stream of text lines(`Line`). - */ -export class LinesDecoder extends BaseDecoder { - /** - * Buffered received data yet to be processed. - */ - private buffer: VSBuffer = VSBuffer.alloc(0); - - /** - * The last emitted `Line` token, if any. The value is used - * to correctly emit remaining line range in the `onStreamEnd` - * method when underlying input stream ends and `buffer` still - * contains some data that must be emitted as the last line. - */ - private lastEmittedLine?: Line; - - /** - * Process data received from the input stream. - */ - protected override onStreamData(chunk: VSBuffer): void { - this.buffer = VSBuffer.concat([this.buffer, chunk]); - - this.processData(false); - } - - /** - * Process buffered data. - * - * @param streamEnded Flag that indicates if the input stream has ended, - * which means that is the last call of this method. - * @throws If internal logic implementation error is detected. - */ - private processData( - streamEnded: boolean, - ): void { - // iterate over each line of the data buffer, emitting each line - // as a `Line` token followed by a `NewLine` token, if applies - while (this.buffer.byteLength > 0) { - // get line number based on a previously emitted line, if any - const lineNumber = this.lastEmittedLine - ? this.lastEmittedLine.range.startLineNumber + 1 - : 1; - - // find the `\r`, `\n`, or `\r\n` tokens in the data - const endOfLineTokens = this.findEndOfLineTokens( - lineNumber, - streamEnded, - ); - const firstToken: (NewLine | CarriageReturn | undefined) = endOfLineTokens[0]; - - // if no end-of-the-line tokens found, stop the current processing - // attempt because we either (1) need more data to be received or - // (2) the stream has ended; in the case (2) remaining data must - // be emitted as the last line - if (firstToken === undefined) { - // (2) if `streamEnded`, we need to emit the whole remaining - // data as the last line immediately - if (streamEnded) { - this.emitLine(lineNumber, this.buffer.slice(0)); - } - - break; - } - - // emit the line found in the data as the `Line` token - this.emitLine(lineNumber, this.buffer.slice(0, firstToken.range.startColumn - 1)); - - // must always hold true as the `emitLine` above sets this - assertDefined( - this.lastEmittedLine, - 'No last emitted line found.', - ); - - // Note! A standalone `\r` token case is not a well-defined case, and - // was primarily used by old Mac OSx systems which treated it as - // a line ending (same as `\n`). Hence for backward compatibility - // with those systems, we treat it as a new line token as well. - // We do that by replacing standalone `\r` token with `\n` one. - if ((endOfLineTokens.length === 1) && (firstToken instanceof CarriageReturn)) { - endOfLineTokens.splice(0, 1, new NewLine(firstToken.range)); - } - - // emit the end-of-the-line tokens - let startColumn = this.lastEmittedLine.range.endColumn; - for (const token of endOfLineTokens) { - const byteLength = token.byte.byteLength; - const endColumn = startColumn + byteLength; - // emit the token updating its column start/end numbers based on - // the emitted line text length and previous end-of-the-line token - this._onData.fire(token.withRange({ startColumn, endColumn })); - // shorten the data buffer by the length of the token - this.buffer = this.buffer.slice(byteLength); - // update the start column for the next token - startColumn = endColumn; - } - } - - // if the stream has ended, assert that the input data buffer is now empty - // otherwise we have a logic error and leaving some buffered data behind - if (streamEnded) { - assert( - this.buffer.byteLength === 0, - 'Expected the input data buffer to be empty when the stream ends.', - ); - } - } - - /** - * Find the end of line tokens in the data buffer. - * Can return: - * - [`\r`, `\n`] tokens if the sequence is found - * - [`\r`] token if only the carriage return is found - * - [`\n`] token if only the newline is found - * - an `empty array` if no end of line tokens found - */ - private findEndOfLineTokens( - lineNumber: number, - streamEnded: boolean, - ): (CarriageReturn | NewLine)[] { - const result = []; - - // find the first occurrence of the carriage return and newline tokens - const carriageReturnIndex = this.buffer.indexOf(CarriageReturn.byte); - const newLineIndex = this.buffer.indexOf(NewLine.byte); - - // if the `\r` comes before the `\n`(if `\n` present at all) - if (carriageReturnIndex >= 0 && ((carriageReturnIndex < newLineIndex) || (newLineIndex === -1))) { - // add the carriage return token first - result.push( - new CarriageReturn(new Range( - lineNumber, - (carriageReturnIndex + 1), - lineNumber, - (carriageReturnIndex + 1) + CarriageReturn.byte.byteLength, - )), - ); - - // if the `\r\n` sequence - if (newLineIndex === carriageReturnIndex + 1) { - // add the newline token to the result - result.push( - new NewLine(new Range( - lineNumber, - (newLineIndex + 1), - lineNumber, - (newLineIndex + 1) + NewLine.byte.byteLength, - )), - ); - } - - // either `\r` or `\r\n` cases found; if we have the `\r` token, we can return - // the end-of-line tokens only, if the `\r` is followed by at least one more - // character (it could be a `\n` or any other character), or if the stream has - // ended (which means the `\r` is at the end of the line) - if ((this.buffer.byteLength > carriageReturnIndex + 1) || streamEnded) { - return result; - } - - // in all other cases, return the empty array (no lend-of-line tokens found) - return []; - } - - // no `\r`, but there is `\n` - if (newLineIndex >= 0) { - result.push( - new NewLine(new Range( - lineNumber, - (newLineIndex + 1), - lineNumber, - (newLineIndex + 1) + NewLine.byte.byteLength, - )), - ); - } - - // neither `\r` nor `\n` found, no end of line found at all - return result; - } - - /** - * Emit a provided line as the `Line` token to the output stream. - */ - private emitLine( - lineNumber: number, // Note! 1-based indexing - lineBytes: VSBuffer, - ): void { - - const line = new Line(lineNumber, lineBytes.toString()); - this._onData.fire(line); - - // store the last emitted line so we can use it when we need - // to send the remaining line in the `onStreamEnd` method - this.lastEmittedLine = line; - - // shorten the data buffer by the length of the line emitted - this.buffer = this.buffer.slice(lineBytes.byteLength); - } - - /** - * Handle the end of the input stream - if the buffer still has some data, - * emit it as the last available line token before firing the `onEnd` event. - */ - protected override onStreamEnd(): void { - // if the input data buffer is not empty when the input stream ends, emit - // the remaining data as the last line before firing the `onEnd` event - if (this.buffer.byteLength > 0) { - this.processData(true); - } - - super.onStreamEnd(); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/tokens/carriageReturn.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/tokens/carriageReturn.ts deleted file mode 100644 index 46b3031829e..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/tokens/carriageReturn.ts +++ /dev/null @@ -1,44 +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 { VSBuffer } from '../../../../../../../../../base/common/buffer.js'; -import { SimpleToken } from '../../simpleCodec/tokens/simpleToken.js'; - -/** - * Token that represent a `carriage return` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class CarriageReturn extends SimpleToken<'\r'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '\r' = '\r'; - - /** - * The byte representation of the {@link symbol}. - */ - public static readonly byte = VSBuffer.fromString(CarriageReturn.symbol); - - /** - * The byte representation of the token. - */ - public get byte(): VSBuffer { - return CarriageReturn.byte; - } - - /** - * Return text representation of the token. - */ - public override get text(): '\r' { - return CarriageReturn.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `CR${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/tokens/line.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/tokens/line.ts deleted file mode 100644 index 2fa0afef6ff..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/tokens/line.ts +++ /dev/null @@ -1,48 +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 { BaseToken } from '../../baseToken.js'; -import { assert } from '../../../../../../../../../base/common/assert.js'; -import { Range } from '../../../../../../../../../editor/common/core/range.js'; - -/** - * Token representing a line of text with a `range` which - * reflects the line's position in the original data. - */ -export class Line extends BaseToken { - constructor( - // the line index - // Note! 1-based indexing - lineNumber: number, - // the line contents - public readonly text: string, - ) { - assert( - !isNaN(lineNumber), - `The line number must not be a NaN.`, - ); - - assert( - lineNumber > 0, - `The line number must be >= 1, got "${lineNumber}".`, - ); - - super( - new Range( - lineNumber, - 1, - lineNumber, - text.length + 1, - ), - ); - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `line("${this.shortText()}")${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/tokens/newLine.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/tokens/newLine.ts deleted file mode 100644 index 6d95751721d..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/linesCodec/tokens/newLine.ts +++ /dev/null @@ -1,44 +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 { VSBuffer } from '../../../../../../../../../base/common/buffer.js'; -import { SimpleToken } from '../../simpleCodec/tokens/simpleToken.js'; - -/** - * A token that represent a `new line` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class NewLine extends SimpleToken<'\n'> { - /** - * The underlying symbol of the `NewLine` token. - */ - public static override readonly symbol: '\n' = '\n'; - - /** - * The byte representation of the {@link symbol}. - */ - public static readonly byte = VSBuffer.fromString(NewLine.symbol); - - /** - * Return text representation of the token. - */ - public override get text(): '\n' { - return NewLine.symbol; - } - - /** - * The byte representation of the token. - */ - public get byte(): VSBuffer { - return NewLine.byte; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `newline${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/markdownDecoder.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/markdownDecoder.ts deleted file mode 100644 index 67bbda1f2ec..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/markdownDecoder.ts +++ /dev/null @@ -1,136 +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 { MarkdownToken } from './tokens/markdownToken.js'; -import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; -import { LeftBracket } from '../simpleCodec/tokens/brackets.js'; -import { PartialMarkdownImage } from './parsers/markdownImage.js'; -import { ReadableStream } from '../../../../../../../../base/common/stream.js'; -import { TSimpleDecoderToken } from '../simpleCodec/simpleDecoder.js'; -import { LeftAngleBracket } from '../simpleCodec/tokens/angleBrackets.js'; -import { ExclamationMark } from '../simpleCodec/tokens/exclamationMark.js'; -import { BaseDecoder } from '../baseDecoder.js'; -import { MarkdownCommentStart, PartialMarkdownCommentStart } from './parsers/markdownComment.js'; -import { MarkdownExtensionsDecoder } from '../markdownExtensionsCodec/markdownExtensionsDecoder.js'; -import { MarkdownLinkCaption, PartialMarkdownLink, PartialMarkdownLinkCaption } from './parsers/markdownLink.js'; - -/** - * Tokens produced by this decoder. - */ -export type TMarkdownToken = MarkdownToken | TSimpleDecoderToken; - -/** - * Decoder capable of parsing markdown entities (e.g., links) from a sequence of simple tokens. - */ -export class MarkdownDecoder extends BaseDecoder { - /** - * Current parser object that is responsible for parsing a sequence of tokens into - * some markdown entity. Set to `undefined` when no parsing is in progress at the moment. - */ - private current?: - PartialMarkdownLinkCaption | MarkdownLinkCaption | PartialMarkdownLink | - PartialMarkdownCommentStart | MarkdownCommentStart | - PartialMarkdownImage; - - constructor( - stream: ReadableStream, - ) { - super(new MarkdownExtensionsDecoder(stream)); - } - - protected override onStreamData(token: TSimpleDecoderToken): void { - // `markdown links` start with `[` character, so here we can - // initiate the process of parsing a markdown link - if (token instanceof LeftBracket && !this.current) { - this.current = new PartialMarkdownLinkCaption(token); - - return; - } - - // `markdown comments` start with `<` character, so here we can - // initiate the process of parsing a markdown comment - if (token instanceof LeftAngleBracket && !this.current) { - this.current = new PartialMarkdownCommentStart(token); - - return; - } - - // `markdown image links` start with `!` character, so here we can - // initiate the process of parsing a markdown image - if (token instanceof ExclamationMark && !this.current) { - this.current = new PartialMarkdownImage(token); - - return; - } - - // if current parser was not initiated before, - we are not inside a sequence - // of tokens we care about, therefore re-emit the token immediately and continue - if (!this.current) { - this._onData.fire(token); - return; - } - - // if there is a current parser object, submit the token to it - // so it can progress with parsing the tokens sequence - const parseResult = this.current.accept(token); - if (parseResult.result === 'success') { - const { nextParser } = parseResult; - - // if got a fully parsed out token back, emit it and reset - // the current parser object so a new parsing process can start - if (nextParser instanceof MarkdownToken) { - this._onData.fire(nextParser); - delete this.current; - } else { - // otherwise, update the current parser object - this.current = nextParser; - } - } else { - // if failed to parse a sequence of a tokens as a single markdown - // entity (e.g., a link), re-emit the tokens accumulated so far - // then reset the current parser object - for (const currentToken of this.current.tokens) { - this._onData.fire(currentToken); - } - - delete this.current; - } - - // if token was not consumed by the parser, call `onStreamData` again - // so the token is properly handled by the decoder in the case when a - // new sequence starts with this token - if (!parseResult.wasTokenConsumed) { - this.onStreamData(token); - } - } - - protected override onStreamEnd(): void { - // if the stream has ended and there is a current incomplete parser - // object present, handle the remaining parser object - if (this.current) { - // if a `markdown comment` does not have an end marker `-->` - // it is still a comment that extends to the end of the file - // so re-emit the current parser as a comment token - if (this.current instanceof MarkdownCommentStart) { - this._onData.fire(this.current.asMarkdownComment()); - delete this.current; - this.onStreamEnd(); - - return; - } - - // in all other cases, re-emit existing parser tokens - const { tokens } = this.current; - - for (const token of [...tokens]) { - this._onData.fire(token); - } - - delete this.current; - } - - super.onStreamEnd(); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/parsers/markdownComment.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/parsers/markdownComment.ts deleted file mode 100644 index a017649f8b8..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/parsers/markdownComment.ts +++ /dev/null @@ -1,173 +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 { Range } from '../../../../../../../../../editor/common/core/range.js'; -import { BaseToken } from '../../baseToken.js'; -import { Dash } from '../../simpleCodec/tokens/dash.js'; -import { assert } from '../../../../../../../../../base/common/assert.js'; -import { MarkdownComment } from '../tokens/markdownComment.js'; -import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; -import { ExclamationMark } from '../../simpleCodec/tokens/exclamationMark.js'; -import { LeftAngleBracket, RightAngleBracket } from '../../simpleCodec/tokens/angleBrackets.js'; -import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; - -/** - * The parser responsible for parsing the ``. If it does, - * then the parser transitions to the {@link MarkdownComment} token. - */ -export class MarkdownCommentStart extends ParserBase { - constructor(tokens: [LeftAngleBracket, ExclamationMark, Dash, Dash]) { - super(tokens); - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - // if received `>` while current token sequence ends with `--`, - // then this is the end of the comment sequence - if (token instanceof RightAngleBracket && this.endsWithDashes) { - this.currentTokens.push(token); - - return { - result: 'success', - nextParser: this.asMarkdownComment(), - wasTokenConsumed: true, - }; - } - - this.currentTokens.push(token); - - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } - - /** - * Convert the current token sequence into a {@link MarkdownComment} token. - * - * Note! that this method marks the current parser object as "consumed" - * hence it should not be used after this method is called. - */ - public asMarkdownComment(): MarkdownComment { - this.isConsumed = true; - - return new MarkdownComment( - this.range, - BaseToken.render(this.currentTokens), - ); - } - - /** - * Get range of current token sequence. - */ - private get range(): Range { - const firstToken = this.currentTokens[0]; - const lastToken = this.currentTokens[this.currentTokens.length - 1]; - - const range = new Range( - firstToken.range.startLineNumber, - firstToken.range.startColumn, - lastToken.range.endLineNumber, - lastToken.range.endColumn, - ); - - return range; - } - - /** - * Whether the current token sequence ends with two dashes. - */ - private get endsWithDashes(): boolean { - const lastToken = this.currentTokens[this.currentTokens.length - 1]; - if (!(lastToken instanceof Dash)) { - return false; - } - - const secondLastToken = this.currentTokens[this.currentTokens.length - 2]; - if (!(secondLastToken instanceof Dash)) { - return false; - } - - return true; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/parsers/markdownImage.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/parsers/markdownImage.ts deleted file mode 100644 index 5684f48de4a..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/parsers/markdownImage.ts +++ /dev/null @@ -1,99 +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 { MarkdownLink } from '../tokens/markdownLink.js'; -import { MarkdownImage } from '../tokens/markdownImage.js'; -import { LeftBracket } from '../../simpleCodec/tokens/brackets.js'; -import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; -import { ExclamationMark } from '../../simpleCodec/tokens/exclamationMark.js'; -import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; -import { MarkdownLinkCaption, PartialMarkdownLink, PartialMarkdownLinkCaption } from './markdownLink.js'; - -/** - * The parser responsible for parsing the `markdown image` sequence of characters. - * E.g., `![alt text](./path/to/image.jpeg)` syntax. - */ -export class PartialMarkdownImage extends ParserBase { - /** - * Current active parser instance, if in the mode of actively parsing the markdown link sequence. - */ - private markdownLinkParser: PartialMarkdownLinkCaption | MarkdownLinkCaption | PartialMarkdownLink | undefined; - - constructor(token: ExclamationMark) { - super([token]); - } - - /** - * Get all currently available tokens of the `markdown link` sequence. - */ - public override get tokens(): readonly TSimpleDecoderToken[] { - const linkTokens = this.markdownLinkParser?.tokens ?? []; - - return [ - ...this.currentTokens, - ...linkTokens, - ]; - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - // on the first call we expect a character that begins `markdown link` sequence - // hence we initiate the markdown link parsing process, otherwise we fail - if (!this.markdownLinkParser) { - if (token instanceof LeftBracket) { - this.markdownLinkParser = new PartialMarkdownLinkCaption(token); - - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } - - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - // handle subsequent tokens next - - const acceptResult = this.markdownLinkParser.accept(token); - const { result, wasTokenConsumed } = acceptResult; - - if (result === 'success') { - const { nextParser } = acceptResult; - - // if full markdown link was parsed out, the process completes - if (nextParser instanceof MarkdownLink) { - this.isConsumed = true; - - const firstToken = this.currentTokens[0]; - return { - result, - wasTokenConsumed, - nextParser: new MarkdownImage( - firstToken.range.startLineNumber, - firstToken.range.startColumn, - `${firstToken.text}${nextParser.caption}`, - nextParser.reference, - ), - }; - } - - // otherwise save new link parser reference and continue - this.markdownLinkParser = nextParser; - return { - result, - wasTokenConsumed, - nextParser: this, - }; - } - - // return the failure result - this.isConsumed = true; - return acceptResult; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/parsers/markdownLink.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/parsers/markdownLink.ts deleted file mode 100644 index 641ddd14b9b..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/parsers/markdownLink.ts +++ /dev/null @@ -1,211 +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 { BaseToken } from '../../baseToken.js'; -import { MarkdownLink } from '../tokens/markdownLink.js'; -import { NewLine } from '../../linesCodec/tokens/newLine.js'; -import { assert } from '../../../../../../../../../base/common/assert.js'; -import { FormFeed } from '../../simpleCodec/tokens/formFeed.js'; -import { VerticalTab } from '../../simpleCodec/tokens/verticalTab.js'; -import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; -import { CarriageReturn } from '../../linesCodec/tokens/carriageReturn.js'; -import { LeftBracket, RightBracket } from '../../simpleCodec/tokens/brackets.js'; -import { ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; -import { LeftParenthesis, RightParenthesis } from '../../simpleCodec/tokens/parentheses.js'; - -/** - * List of characters that are not allowed in links so stop a markdown link sequence abruptly. - */ -const MARKDOWN_LINK_STOP_CHARACTERS: readonly string[] = [CarriageReturn, NewLine, VerticalTab, FormFeed] - .map((token) => { return token.symbol; }); - -/** - * The parser responsible for parsing a `markdown link caption` part of a markdown - * link (e.g., the `[caption text]` part of the `[caption text](./some/path)` link). - * - * 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 {@link MarkdownLinkCaption} parser type which continues the general - * parsing process of the markdown link. - * - * Otherwise, if one of the stop characters defined in the {@link MARKDOWN_LINK_STOP_CHARACTERS} - * is encountered before the `]` token, the parsing process is aborted which is communicated to - * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible - * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no - * longer represent a coherent token entity of a larger size. - */ -export class PartialMarkdownLinkCaption extends ParserBase { - constructor(token: LeftBracket) { - super([token]); - } - - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - // any of stop characters is are breaking a markdown link caption sequence - if (MARKDOWN_LINK_STOP_CHARACTERS.includes(token.text)) { - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - // the `]` character ends the caption of a markdown link - if (token instanceof RightBracket) { - return { - result: 'success', - nextParser: new MarkdownLinkCaption([...this.tokens, token]), - wasTokenConsumed: true, - }; - } - - // otherwise, include the token in the sequence - // and keep the current parser object instance - this.currentTokens.push(token); - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } -} - -/** - * The parser responsible for transitioning from a {@link PartialMarkdownLinkCaption} - * parser to the {@link PartialMarkdownLink} one, therefore serves a parser glue between - * the `[caption]` and the `(./some/path)` parts of the `[caption](./some/path)` link. - * - * The only successful case of this parser is the `(` token that initiated the process - * of parsing the `reference` part of a markdown link and in this case the parser - * transitions into the `PartialMarkdownLink` parser type. - * - * Any other character is considered a failure result. In this case, the caller is assumed - * to be responsible for re-emitting the {@link tokens} accumulated so far as standalone - * entities since they are no longer represent a coherent token entity of a larger size. - */ -export class MarkdownLinkCaption extends ParserBase { - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - // the `(` character starts the link part of a markdown link - // that is the only character that can follow the caption - if (token instanceof LeftParenthesis) { - return { - result: 'success', - wasTokenConsumed: true, - nextParser: new PartialMarkdownLink([...this.tokens], token), - }; - } - - return { - result: 'failure', - wasTokenConsumed: false, - }; - } -} - -/** - * The parser responsible for parsing a `link reference` part of a markdown link - * (e.g., the `(./some/path)` part of the `[caption text](./some/path)` link). - * - * The parsing process starts with tokens that represent the `[caption]` part of a markdown - * link, followed by the `(` token. The parser collects all subsequent tokens until final closing - * parenthesis (`)`) is encountered (*\*see [1] below*). In this successful case, the parser object - * transitions into the {@link MarkdownLink} token type which signifies the end of the entire - * parsing process of the link text. - * - * Otherwise, if one of the stop characters defined in the {@link MARKDOWN_LINK_STOP_CHARACTERS} - * is encountered before the final `)` token, the parsing process is aborted which is communicated to - * the caller by returning a `failure` result. In this case, the caller is assumed to be responsible - * for re-emitting the {@link tokens} accumulated so far as standalone entities since they are no - * longer represent a coherent token entity of a larger size. - * - * `[1]` The `reference` part of the markdown link can contain any number of nested parenthesis, e.g., - * `[caption](/some/p(th/file.md)` is a valid markdown link and a valid folder name, hence number - * of open parenthesis must match the number of closing ones and the path sequence is considered - * to be complete as soon as this requirement is met. Therefore the `final` word is used in - * the description comments above to highlight this important detail. - */ -export class PartialMarkdownLink extends ParserBase { - /** - * Number of open parenthesis in the sequence. - * See comment in the {@link accept} method for more details. - */ - private openParensCount: number = 1; - - constructor( - protected readonly captionTokens: TSimpleDecoderToken[], - token: LeftParenthesis, - ) { - super([token]); - } - - public override get tokens(): readonly TSimpleDecoderToken[] { - return [...this.captionTokens, ...this.currentTokens]; - } - - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - // markdown links allow for nested parenthesis inside the link reference part, but - // the number of open parenthesis must match the number of closing parenthesis, e.g.: - // - `[caption](/some/p()th/file.md)` is a valid markdown link - // - `[caption](/some/p(th/file.md)` is an invalid markdown link - // hence we use the `openParensCount` variable to keep track of the number of open - // parenthesis encountered so far; then upon encountering a closing parenthesis we - // decrement the `openParensCount` and if it reaches 0 - we consider the link reference - // to be complete - - if (token instanceof LeftParenthesis) { - this.openParensCount += 1; - } - - if (token instanceof RightParenthesis) { - this.openParensCount -= 1; - - // sanity check! this must alway hold true because we return a complete markdown - // link as soon as we encounter matching number of closing parenthesis, hence - // we must never have `openParensCount` that is less than 0 - assert( - this.openParensCount >= 0, - `Unexpected right parenthesis token encountered: '${token}'.`, - ); - - // the markdown link is complete as soon as we get the same number of closing parenthesis - if (this.openParensCount === 0) { - const { startLineNumber, startColumn } = this.captionTokens[0].range; - - // create link caption string - const caption = BaseToken.render(this.captionTokens); - - // create link reference string - this.currentTokens.push(token); - const reference = BaseToken.render(this.currentTokens); - - // return complete markdown link object - return { - result: 'success', - wasTokenConsumed: true, - nextParser: new MarkdownLink( - startLineNumber, - startColumn, - caption, - reference, - ), - }; - } - } - - // any of stop characters is are breaking a markdown link reference sequence - if (MARKDOWN_LINK_STOP_CHARACTERS.includes(token.text)) { - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - // the rest of the tokens can be included in the sequence - this.currentTokens.push(token); - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownComment.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownComment.ts deleted file mode 100644 index 8eadff3589a..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownComment.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 { Range } from '../../../../../../../../../editor/common/core/range.js'; -import { MarkdownToken } from './markdownToken.js'; -import { assert } from '../../../../../../../../../base/common/assert.js'; - -/** - * A token that represent a `markdown comment` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class MarkdownComment extends MarkdownToken { - constructor( - range: Range, - public readonly text: string, - ) { - assert( - text.startsWith('`. - */ - public get hasEndMarker(): boolean { - return this.text.endsWith('-->'); - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `md-comment("${this.shortText()}")${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownImage.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownImage.ts deleted file mode 100644 index cab120f53db..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownImage.ts +++ /dev/null @@ -1,125 +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 { MarkdownToken } from './markdownToken.js'; -import { IRange, Range } from '../../../../../../../../../editor/common/core/range.js'; -import { assert } from '../../../../../../../../../base/common/assert.js'; - -/** - * A token that represent a `markdown image` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class MarkdownImage extends MarkdownToken { - /** - * Check if this `markdown image link` points to a valid URL address. - */ - public readonly isURL: boolean; - - constructor( - /** - * The starting line number of the image (1-based indexing). - */ - lineNumber: number, - /** - * The starting column number of the image (1-based indexing). - */ - columnNumber: number, - /** - * The caption of the image, including the `!` and `square brackets`. - */ - private readonly caption: string, - /** - * The reference of the image, including the parentheses. - */ - private readonly reference: string, - ) { - assert( - !isNaN(lineNumber), - `The line number must not be a NaN.`, - ); - - assert( - lineNumber > 0, - `The line number must be >= 1, got "${lineNumber}".`, - ); - - assert( - columnNumber > 0, - `The column number must be >= 1, got "${columnNumber}".`, - ); - - assert( - caption[0] === '!', - `The caption must start with '!' character, got "${caption}".`, - ); - - assert( - caption[1] === '[' && caption[caption.length - 1] === ']', - `The caption must be enclosed in square brackets, got "${caption}".`, - ); - - assert( - reference[0] === '(' && reference[reference.length - 1] === ')', - `The reference must be enclosed in parentheses, got "${reference}".`, - ); - - super( - new Range( - lineNumber, - columnNumber, - lineNumber, - columnNumber + caption.length + reference.length, - ), - ); - - // set up the `isURL` flag based on the current - try { - new URL(this.path); - this.isURL = true; - } catch { - this.isURL = false; - } - } - - public override get text(): string { - return `${this.caption}${this.reference}`; - } - - /** - * Returns the `reference` part of the link without enclosing parentheses. - */ - public get path(): string { - return this.reference.slice(1, this.reference.length - 1); - } - - /** - * Get the range of the `link part` of the token. - */ - public get linkRange(): IRange | undefined { - if (this.path.length === 0) { - return undefined; - } - - const { range } = this; - - // note! '+1' for openning `(` of the link - const startColumn = range.startColumn + this.caption.length + 1; - const endColumn = startColumn + this.path.length; - - return new Range( - range.startLineNumber, - startColumn, - range.endLineNumber, - endColumn, - ); - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `md-image("${this.shortText()}")${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownLink.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownLink.ts deleted file mode 100644 index a963628675d..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownCodec/tokens/markdownLink.ts +++ /dev/null @@ -1,120 +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 { MarkdownToken } from './markdownToken.js'; -import { IRange, Range } from '../../../../../../../../../editor/common/core/range.js'; -import { assert } from '../../../../../../../../../base/common/assert.js'; - -/** - * A token that represent a `markdown link` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class MarkdownLink extends MarkdownToken { - /** - * Check if this `markdown link` points to a valid URL address. - */ - public readonly isURL: boolean; - - constructor( - /** - * The starting line number of the link (1-based indexing). - */ - lineNumber: number, - /** - * The starting column number of the link (1-based indexing). - */ - columnNumber: number, - /** - * The caption of the original link, including the square brackets. - */ - public readonly caption: string, - /** - * The reference of the original link, including the parentheses. - */ - public readonly reference: string, - ) { - assert( - !isNaN(lineNumber), - `The line number must not be a NaN.`, - ); - - assert( - lineNumber > 0, - `The line number must be >= 1, got "${lineNumber}".`, - ); - - assert( - columnNumber > 0, - `The column number must be >= 1, got "${columnNumber}".`, - ); - - assert( - caption[0] === '[' && caption[caption.length - 1] === ']', - `The caption must be enclosed in square brackets, got "${caption}".`, - ); - - assert( - reference[0] === '(' && reference[reference.length - 1] === ')', - `The reference must be enclosed in parentheses, got "${reference}".`, - ); - - super( - new Range( - lineNumber, - columnNumber, - lineNumber, - columnNumber + caption.length + reference.length, - ), - ); - - // set up the `isURL` flag based on the current - try { - new URL(this.path); - this.isURL = true; - } catch { - this.isURL = false; - } - } - - public override get text(): string { - return `${this.caption}${this.reference}`; - } - - /** - * Returns the `reference` part of the link without enclosing parentheses. - */ - public get path(): string { - return this.reference.slice(1, this.reference.length - 1); - } - - /** - * Get the range of the `link part` of the token. - */ - public get linkRange(): IRange | undefined { - if (this.path.length === 0) { - return undefined; - } - - const { range } = this; - - // note! '+1' for opening `(` of the link - const startColumn = range.startColumn + this.caption.length + 1; - const endColumn = startColumn + this.path.length; - - return new Range( - range.startLineNumber, - startColumn, - range.endLineNumber, - endColumn, - ); - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `md-link("${this.shortText()}")${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/markdownExtensionsDecoder.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/markdownExtensionsDecoder.ts deleted file mode 100644 index b5ff3d2b9f9..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/markdownExtensionsDecoder.ts +++ /dev/null @@ -1,119 +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 { VSBuffer } from '../../../../../../../../base/common/buffer.js'; -import { ReadableStream } from '../../../../../../../../base/common/stream.js'; -import { BaseDecoder } from '../baseDecoder.js'; -import { MarkdownExtensionsToken } from './tokens/markdownExtensionsToken.js'; -import { SimpleDecoder, TSimpleDecoderToken } from '../simpleCodec/simpleDecoder.js'; -import { PartialFrontMatterHeader, PartialFrontMatterStartMarker } from './parsers/frontMatterHeader.js'; - -/** - * Tokens produced by this decoder. - */ -export type TMarkdownExtensionsToken = MarkdownExtensionsToken | TSimpleDecoderToken; - -/** - * Decoder responsible for decoding extensions of markdown syntax, - * e.g., a `Front Matter` header, etc. - */ -export class MarkdownExtensionsDecoder extends BaseDecoder { - /** - * Current parser object that is responsible for parsing a sequence of tokens into - * some markdown entity. Set to `undefined` when no parsing is in progress at the moment. - */ - private current?: PartialFrontMatterStartMarker | PartialFrontMatterHeader; - - constructor( - stream: ReadableStream, - ) { - super(new SimpleDecoder(stream)); - } - - protected override onStreamData(token: TSimpleDecoderToken): void { - // front matter headers start with a `-` at the first column of the first line - if ((this.current === undefined) && PartialFrontMatterStartMarker.mayStartHeader(token)) { - this.current = new PartialFrontMatterStartMarker(token); - - return; - } - - // if current parser is not initiated, - we are not inside a sequence of tokens - // we care about, therefore re-emit the token immediately and continue - if (this.current === undefined) { - this._onData.fire(token); - return; - } - - // if there is a current parser object, submit the token to it - // so it can progress with parsing the tokens sequence - const parseResult = this.current.accept(token); - if (parseResult.result === 'success') { - const { nextParser } = parseResult; - - // if got a fully parsed out token back, emit it and reset - // the current parser object so a new parsing process can start - if (nextParser instanceof MarkdownExtensionsToken) { - this._onData.fire(nextParser); - delete this.current; - } else { - // otherwise, update the current parser object - this.current = nextParser; - } - } else { - // if failed to parse a sequence of a tokens as a single markdown - // entity (e.g., a link), re-emit the tokens accumulated so far - // then reset the currently initialized parser object - this.reEmitCurrentTokens(); - } - - // if token was not consumed by the parser, call `onStreamData` again - // so the token is properly handled by the decoder in the case when a - // new sequence starts with this token - if (!parseResult.wasTokenConsumed) { - this.onStreamData(token); - } - } - - protected override onStreamEnd(): void { - try { - if (this.current === undefined) { - return; - } - - // if current parser can be converted into a valid Front Matter - // header, then emit it and reset the current parser object - if (this.current instanceof PartialFrontMatterHeader) { - this._onData.fire( - this.current.asFrontMatterHeader(), - ); - delete this.current; - return; - } - - } catch { - // if failed to convert current parser object to a token, - // re-emit the tokens accumulated so far - this.reEmitCurrentTokens(); - } finally { - delete this.current; - super.onStreamEnd(); - } - } - - /** - * Re-emit tokens accumulated so far in the current parser object. - */ - protected reEmitCurrentTokens(): void { - if (this.current === undefined) { - return; - } - - for (const token of this.current.tokens) { - this._onData.fire(token); - } - delete this.current; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/parsers/frontMatterHeader.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/parsers/frontMatterHeader.ts deleted file mode 100644 index a63c911174c..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/parsers/frontMatterHeader.ts +++ /dev/null @@ -1,345 +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 { Dash } from '../../simpleCodec/tokens/dash.js'; -import { NewLine } from '../../linesCodec/tokens/newLine.js'; -import { FrontMatterHeader } from '../tokens/frontMatterHeader.js'; -import { assertDefined } from '../../../../../../../../../base/common/types.js'; -import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; -import { assert, assertNever } from '../../../../../../../../../base/common/assert.js'; -import { CarriageReturn } from '../../linesCodec/tokens/carriageReturn.js'; -import { FrontMatterMarker, TMarkerToken } from '../tokens/frontMatterMarker.js'; -import { assertNotConsumed, IAcceptTokenSuccess, ParserBase, TAcceptTokenResult } from '../../simpleCodec/parserBase.js'; - -/** - * Parses the start marker of a Front Matter header. - */ -export class PartialFrontMatterStartMarker extends ParserBase { - constructor(token: Dash) { - const { range } = token; - - assert( - range.startLineNumber === 1, - `Front Matter header must start at the first line, but it starts at line #${range.startLineNumber}.`, - ); - - assert( - range.startColumn === 1, - `Front Matter header must start at the beginning of the line, but it starts at ${range.startColumn}.`, - ); - - super([token]); - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - const previousToken = this.currentTokens[this.currentTokens.length - 1]; - - // collect a sequence of dash tokens that may end with a CR token - if ((token instanceof Dash) || (token instanceof CarriageReturn)) { - // a dash or CR tokens can go only after another dash token - if ((previousToken instanceof Dash) === false) { - this.isConsumed = true; - - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - this.currentTokens.push(token); - - return { - result: 'success', - wasTokenConsumed: true, - nextParser: this, - }; - } - - // stop collecting dash tokens when a new line token is encountered - if (token instanceof NewLine) { - this.isConsumed = true; - - return { - result: 'success', - wasTokenConsumed: true, - nextParser: new PartialFrontMatterHeader( - FrontMatterMarker.fromTokens([ - ...this.currentTokens, - token, - ]), - ), - }; - } - - // any other token is invalid for the `start marker` - this.isConsumed = true; - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - /** - * Check if provided dash token can be a start of a Front Matter header. - */ - public static mayStartHeader(token: TSimpleDecoderToken): token is Dash { - return (token instanceof Dash) - && (token.range.startLineNumber === 1) - && (token.range.startColumn === 1); - } -} - -/** - * Parses a Front Matter header that already has a start marker - * and possibly some content that follows. - */ -export class PartialFrontMatterHeader extends ParserBase { - /** - * Parser instance for the end marker of the Front Matter header. - */ - private maybeEndMarker?: PartialFrontMatterEndMarker; - - constructor( - public readonly startMarker: FrontMatterMarker, - ) { - super([]); - } - - public override get tokens(): readonly TSimpleDecoderToken[] { - const endMarkerTokens = (this.maybeEndMarker !== undefined) - ? this.maybeEndMarker.tokens - : []; - - return [ - ...this.startMarker.tokens, - ...this.currentTokens, - ...endMarkerTokens, - ]; - } - - /** - * Convert the current token sequence into a {@link FrontMatterHeader} token. - * - * Note! that this method marks the current parser object as "consumed" - * hence it should not be used after this method is called. - */ - public asFrontMatterHeader(): FrontMatterHeader { - assertDefined( - this.maybeEndMarker, - 'Cannot convert to Front Matter header token without an end marker.', - ); - - assert( - this.maybeEndMarker.dashCount === this.startMarker.dashTokens.length, - [ - 'Start and end markers must have the same number of dashes', - `, got ${this.startMarker.dashTokens.length} / ${this.maybeEndMarker.dashCount}.`, - ].join(''), - ); - - this.isConsumed = true; - - return FrontMatterHeader.fromTokens( - this.startMarker.tokens, - this.currentTokens, - this.maybeEndMarker.tokens, - ); - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - // if in the mode of parsing the end marker sequence, forward - // the token to the current end marker parser instance - if (this.maybeEndMarker !== undefined) { - return this.acceptEndMarkerToken(token); - } - - // collect all tokens until a `dash token at the beginning of a line` is found - if (((token instanceof Dash) === false) || (token.range.startColumn !== 1)) { - this.currentTokens.push(token); - - return { - result: 'success', - wasTokenConsumed: true, - nextParser: this, - }; - } - - // a dash token at the beginning of the line might be a start of the `end marker` - // sequence of the front matter header, hence initialize appropriate parser object - assert( - this.maybeEndMarker === undefined, - `End marker parser must not be present.`, - ); - this.maybeEndMarker = new PartialFrontMatterEndMarker(token); - - return { - result: 'success', - wasTokenConsumed: true, - nextParser: this, - }; - } - - /** - * When a end marker parser is present, we pass all tokens to it - * until it is completes the parsing process(either success or failure). - */ - private acceptEndMarkerToken( - token: TSimpleDecoderToken, - ): TAcceptTokenResult { - assertDefined( - this.maybeEndMarker, - `Partial end marker parser must be initialized.`, - ); - - // if we have a partial end marker, we are in the process of parsing - // the end marker, so just pass the token to it and return - const acceptResult = this.maybeEndMarker.accept(token); - const { result, wasTokenConsumed } = acceptResult; - - if (result === 'success') { - const { nextParser } = acceptResult; - const endMarkerParsingComplete = (nextParser instanceof FrontMatterMarker); - - if (endMarkerParsingComplete === false) { - return { - result: 'success', - wasTokenConsumed, - nextParser: this, - }; - } - - const endMarker = nextParser; - - // start and end markers must have the same number of dashes, hence - // if they don't match, we would like to continue parsing the header - // until we find an end marker with the same number of dashes - if (endMarker.dashTokens.length !== this.startMarker.dashTokens.length) { - return this.handleEndMarkerParsingFailure( - endMarker.tokens, - wasTokenConsumed, - ); - } - - this.isConsumed = true; - return { - result: 'success', - wasTokenConsumed: true, - nextParser: FrontMatterHeader.fromTokens( - this.startMarker.tokens, - this.currentTokens, - this.maybeEndMarker.tokens, - ), - }; - } - - // if failed to parse the end marker, we would like to continue parsing - // the header until we find a valid end marker - if (result === 'failure') { - return this.handleEndMarkerParsingFailure( - this.maybeEndMarker.tokens, - wasTokenConsumed, - ); - } - - assertNever( - result, - `Unexpected result '${result}' while parsing the end marker.`, - ); - } - - /** - * On failure to parse the end marker, we need to continue parsing - * the header because there might be another valid end marker in - * the stream of tokens. Therefore we copy over the end marker tokens - * into the list of "content" tokens and reset the end marker parser. - */ - private handleEndMarkerParsingFailure( - tokens: readonly TSimpleDecoderToken[], - wasTokenConsumed: boolean, - ): IAcceptTokenSuccess { - this.currentTokens.push(...tokens); - delete this.maybeEndMarker; - - return { - result: 'success', - wasTokenConsumed, - nextParser: this, - }; - } -} - -/** - * Parser the end marker sequence of a Front Matter header. - */ -class PartialFrontMatterEndMarker extends ParserBase { - constructor(token: Dash) { - const { range } = token; - - assert( - range.startColumn === 1, - `Front Matter header must start at the beginning of the line, but it starts at ${range.startColumn}.`, - ); - - super([token]); - } - - /** - * Number of dashes in the marker. - */ - public get dashCount(): number { - return this.tokens - .filter((token) => { return token instanceof Dash; }) - .length; - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - const previousToken = this.currentTokens[this.currentTokens.length - 1]; - - // collect a sequence of dash tokens that may end with a CR token - if ((token instanceof Dash) || (token instanceof CarriageReturn)) { - // a dash or CR tokens can go only after another dash token - if ((previousToken instanceof Dash) === false) { - this.isConsumed = true; - - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - this.currentTokens.push(token); - - return { - result: 'success', - wasTokenConsumed: true, - nextParser: this, - }; - } - - // stop collecting dash tokens when a new line token is encountered - if (token instanceof NewLine) { - this.isConsumed = true; - this.currentTokens.push(token); - - return { - result: 'success', - wasTokenConsumed: true, - nextParser: FrontMatterMarker.fromTokens([ - ...this.currentTokens, - ]), - }; - } - - // any other token is invalid for the `start marker` - this.isConsumed = true; - return { - result: 'failure', - wasTokenConsumed: false, - }; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/tokens/frontMatterHeader.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/tokens/frontMatterHeader.ts deleted file mode 100644 index b5b3903b20a..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/tokens/frontMatterHeader.ts +++ /dev/null @@ -1,79 +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 { Text } from '../../textToken.js'; -import { Range } from '../../../../../../../../../editor/common/core/range.js'; -import { BaseToken } from '../../baseToken.js'; -import { MarkdownExtensionsToken } from './markdownExtensionsToken.js'; -import { TSimpleDecoderToken } from '../../simpleCodec/simpleDecoder.js'; -import { FrontMatterMarker, TMarkerToken } from './frontMatterMarker.js'; - -/** - * Token that represents a `Front Matter` header in a text. - */ -export class FrontMatterHeader extends MarkdownExtensionsToken { - constructor( - range: Range, - public readonly startMarker: FrontMatterMarker, - public readonly content: Text, - public readonly endMarker: FrontMatterMarker, - ) { - super(range); - } - - /** - * Return complete text representation of the token. - */ - public get text(): string { - const text: string[] = [ - this.startMarker.text, - this.content.text, - this.endMarker.text, - ]; - - return text.join(''); - } - - /** - * Range of the content of the Front Matter header. - */ - public get contentRange(): Range { - return this.content.range; - } - - /** - * Content token of the Front Matter header. - */ - public get contentToken(): Text { - return this.content; - } - - /** - * Create new instance of the token from the given tokens. - */ - public static fromTokens( - startMarkerTokens: readonly TMarkerToken[], - contentTokens: readonly TSimpleDecoderToken[], - endMarkerTokens: readonly TMarkerToken[], - ): FrontMatterHeader { - const range = BaseToken.fullRange( - [...startMarkerTokens, ...endMarkerTokens], - ); - - return new FrontMatterHeader( - range, - FrontMatterMarker.fromTokens(startMarkerTokens), - new Text(contentTokens), - FrontMatterMarker.fromTokens(endMarkerTokens), - ); - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `frontmatter("${this.shortText()}")${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/tokens/frontMatterMarker.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/tokens/frontMatterMarker.ts deleted file mode 100644 index 14813b3116b..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/markdownExtensionsCodec/tokens/frontMatterMarker.ts +++ /dev/null @@ -1,59 +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 { Range } from '../../../../../../../../../editor/common/core/range.js'; -import { BaseToken } from '../../baseToken.js'; -import { Dash } from '../../simpleCodec/tokens/dash.js'; -import { NewLine } from '../../linesCodec/tokens/newLine.js'; -import { MarkdownExtensionsToken } from './markdownExtensionsToken.js'; -import { CarriageReturn } from '../../linesCodec/tokens/carriageReturn.js'; - -/** - * Type for tokens inside a Front Matter header marker. - */ -export type TMarkerToken = Dash | CarriageReturn | NewLine; - -/** - * Marker for the start and end of a Front Matter header. - */ -export class FrontMatterMarker extends MarkdownExtensionsToken { - /** - * Returns complete text representation of the token. - */ - public get text(): string { - return BaseToken.render(this.tokens); - } - - /** - * List of {@link Dash} tokens in the marker. - */ - public get dashTokens(): readonly Dash[] { - return this.tokens - .filter((token) => { return token instanceof Dash; }); - } - - constructor( - range: Range, - public readonly tokens: readonly TMarkerToken[], - ) { - super(range); - } - - /** - * Create new instance of the token from a provided - * list of tokens. - */ - public static fromTokens( - tokens: readonly TMarkerToken[], - ): FrontMatterMarker { - const range = BaseToken.fullRange(tokens); - - return new FrontMatterMarker(range, tokens); - } - - public toString(): string { - return `frontmatter-marker(${this.dashTokens.length}:${this.range})`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/parserBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/parserBase.ts deleted file mode 100644 index 84515dc1d6c..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/parserBase.ts +++ /dev/null @@ -1,137 +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 { BaseToken } from '../baseToken.js'; -import { assert } from '../../../../../../../../base/common/assert.js'; - -/** - * Common interface for a result of accepting a next token - * in a sequence. - */ -export interface IAcceptTokenResult { - /** - * The result type of accepting a next token in a sequence. - */ - result: 'success' | 'failure'; - - /** - * Whether the token to accept was consumed by the parser - * during the accept operation. - */ - wasTokenConsumed: boolean; -} - -/** - * Successful result of accepting a next token in a sequence. - */ -export interface IAcceptTokenSuccess extends IAcceptTokenResult { - result: 'success'; - nextParser: T; -} - -/** - * Failure result of accepting a next token in a sequence. - */ -export interface IAcceptTokenFailure extends IAcceptTokenResult { - result: 'failure'; -} - -/** - * The result of operation of accepting a next token in a sequence. - */ -export type TAcceptTokenResult = IAcceptTokenSuccess | IAcceptTokenFailure; - -/** - * An abstract parser class that is able to parse a sequence of - * tokens into a new single entity. - */ -export abstract class ParserBase { - /** - * Whether the parser object was "consumed" and should not be used anymore. - */ - 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. - */ - protected readonly startTokensCount: number; - - constructor( - /** - * Set of tokens that were accumulated so far. - */ - protected readonly currentTokens: TToken[] = [], - ) { - this.startTokensCount = this.currentTokens.length; - } - - /** - * Get the tokens that were accumulated so far. - */ - public get tokens(): readonly TToken[] { - return this.currentTokens; - } - - /** - * Accept a new token returning parsing result: - * - successful result must include the next parser object or a fully parsed out token - * - failure result must indicate that the token was not consumed - * - * @param token The token to accept. - * @returns The parsing result. - */ - public abstract accept(token: TToken): TAcceptTokenResult; - - /** - * A helper method that validates that the current parser object was not yet consumed, - * hence can still be used to accept new tokens in the parsing process. - * - * @throws if the parser object is already consumed. - */ - protected assertNotConsumed(): void { - assert( - this.isConsumed === false, - `The parser object is already consumed and should not be used anymore.`, - ); - } -} - -/** - * Decorator that validates that the current parser object was not yet consumed, - * hence can still be used to accept new tokens in the parsing process. - * - * @throws the resulting decorated method throws if the parser object was already consumed. - */ -export function assertNotConsumed>( - _target: T, - propertyKey: 'accept', - descriptor: PropertyDescriptor, -): PropertyDescriptor { - // store the original method reference - const originalMethod = descriptor.value; - - // validate that the current parser object was not yet consumed - // before invoking the original accept method - descriptor.value = function ( - this: T, - ...args: Parameters - ): ReturnType { - assert( - this.isConsumed === false, - `The parser object is already consumed and should not be used anymore.`, - ); - - return originalMethod.apply(this, args); - }; - - return descriptor; -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/simpleDecoder.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/simpleDecoder.ts deleted file mode 100644 index 9addf4df2ca..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/simpleDecoder.ts +++ /dev/null @@ -1,132 +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 { NewLine } from '../linesCodec/tokens/newLine.js'; -import { CarriageReturn } from '../linesCodec/tokens/carriageReturn.js'; -import { LinesDecoder, TLineBreakToken, TLineToken } from '../linesCodec/linesDecoder.js'; -import { - At, - Tab, - Word, - Hash, - Dash, - Colon, - Slash, - Space, - Quote, - Comma, - FormFeed, - DollarSign, - DoubleQuote, - VerticalTab, - type TBracket, - LeftBracket, - RightBracket, - type TCurlyBrace, - LeftCurlyBrace, - RightCurlyBrace, - ExclamationMark, - type TParenthesis, - LeftParenthesis, - RightParenthesis, - type TAngleBracket, - LeftAngleBracket, - RightAngleBracket, -} from './tokens/tokens.js'; -import { ISimpleTokenClass, SimpleToken } from './tokens/simpleToken.js'; -import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; -import { BaseDecoder } from '../baseDecoder.js'; -import { ReadableStream } from '../../../../../../../../base/common/stream.js'; - -/** - * Type for all simple tokens. - */ -export type TSimpleToken = Space | Tab | VerticalTab | At | Quote | DoubleQuote - | CarriageReturn | NewLine | FormFeed | TBracket | TAngleBracket | TCurlyBrace - | TParenthesis | Colon | Hash | Dash | ExclamationMark | Slash | DollarSign | Comma - | TLineBreakToken; - -/** -* Type of tokens emitted by this decoder. -*/ -export type TSimpleDecoderToken = TSimpleToken | Word; - -/** - * List of well-known distinct tokens that this decoder emits (excluding - * the word stop characters defined below). Everything else is considered - * an arbitrary "text" sequence and is emitted as a single {@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, Quote, DoubleQuote, Comma, -]); - -/** - * A {@link Word} sequence stops when one of the well-known tokens are encountered. - * Note! the `\r` and `\n` are excluded from the list because this decoder based on - * the {@link LinesDecoder} which emits {@link Line} tokens without them. - */ -const WORD_STOP_CHARACTERS: readonly string[] = Object.freeze( - WELL_KNOWN_TOKENS.map(token => token.symbol), -); - -/** - * A decoder that can decode a stream of `Line`s into a stream - * of simple token, - `Word`, `Space`, `Tab`, `NewLine`, etc. - */ -export class SimpleDecoder extends BaseDecoder { - constructor( - stream: ReadableStream, - ) { - super(new LinesDecoder(stream)); - } - - protected override onStreamData(line: TLineToken): void { - // re-emit new line tokens immediately - if (line instanceof CarriageReturn || line instanceof NewLine) { - this._onData.fire(line); - - return; - } - - // loop through the text separating it into `Word` and `well-known` tokens - const lineText = line.text.split(''); - let i = 0; - while (i < lineText.length) { - // index is 0-based, but column numbers are 1-based - const columnNumber = i + 1; - const character = lineText[i]; - - // check if the current character is a well-known token - const tokenConstructor = WELL_KNOWN_TOKENS - .find((wellKnownToken) => { - return wellKnownToken.symbol === character; - }); - - // if it is a well-known token, emit it and continue to the next one - if (tokenConstructor) { - this._onData.fire(SimpleToken.newOnLine(line, columnNumber, tokenConstructor)); - - i++; - continue; - } - - // otherwise, it is an arbitrary "text" sequence of characters, - // that needs to be collected into a single `Word` token, hence - // read all the characters until a stop character is encountered - let word = ''; - while (i < lineText.length && !(WORD_STOP_CHARACTERS.includes(lineText[i]))) { - word += lineText[i]; - i++; - } - - // emit a "text" sequence of characters as a single `Word` token - this._onData.fire( - Word.newOnLine(word, line, columnNumber), - ); - } - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/angleBrackets.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/angleBrackets.ts deleted file mode 100644 index d57acedc1c3..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/angleBrackets.ts +++ /dev/null @@ -1,61 +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 { SimpleToken } from './simpleToken.js'; - -/** - * A token that represent a `<` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class LeftAngleBracket extends SimpleToken<'<'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '<' = '<'; - - /** - * Return text representation of the token. - */ - public override get text(): '<' { - return LeftAngleBracket.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `left-angle-bracket${this.range}`; - } -} - -/** - * A token that represent a `>` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class RightAngleBracket extends SimpleToken<'>'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '>' = '>'; - - /** - * Return text representation of the token. - */ - public override get text(): '>' { - return RightAngleBracket.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `right-angle-bracket${this.range}`; - } -} - -/** - * General angle bracket token type. - */ -export type TAngleBracket = LeftAngleBracket | RightAngleBracket; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/at.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/at.ts deleted file mode 100644 index 25c44433afd..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/at.ts +++ /dev/null @@ -1,31 +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 { SimpleToken } from './simpleToken.js'; - -/** - * A token that represent a `@` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class At extends SimpleToken<'@'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '@' = '@'; - - /** - * Return text representation of the token. - */ - public override get text(): '@' { - return At.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `at${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/brackets.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/brackets.ts deleted file mode 100644 index 491cb2a588c..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/brackets.ts +++ /dev/null @@ -1,61 +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 { SimpleToken } from './simpleToken.js'; - -/** - * A token that represent a `[` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class LeftBracket extends SimpleToken<'['> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '[' = '['; - - /** - * Return text representation of the token. - */ - public override get text(): '[' { - return LeftBracket.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `left-bracket${this.range}`; - } -} - -/** - * A token that represent a `]` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class RightBracket extends SimpleToken<']'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: ']' = ']'; - - /** - * Return text representation of the token. - */ - public override get text(): ']' { - return RightBracket.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `right-bracket${this.range}`; - } -} - -/** - * General bracket token type. - */ -export type TBracket = LeftBracket | RightBracket; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/colon.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/colon.ts deleted file mode 100644 index a04b3e853df..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/colon.ts +++ /dev/null @@ -1,31 +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 { SimpleToken } from './simpleToken.js'; - -/** - * A token that represent a `:` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class Colon extends SimpleToken<':'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: ':' = ':'; - - /** - * Return text representation of the token. - */ - public override get text(): ':' { - return Colon.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `colon${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/comma.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/comma.ts deleted file mode 100644 index ce5df1748ca..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/comma.ts +++ /dev/null @@ -1,31 +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 { 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/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/curlyBraces.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/curlyBraces.ts deleted file mode 100644 index 9c13e501d27..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/curlyBraces.ts +++ /dev/null @@ -1,61 +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 { SimpleToken } from './simpleToken.js'; - -/** - * A token that represent a `{` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class LeftCurlyBrace extends SimpleToken<'{'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '{' = '{'; - - /** - * Return text representation of the token. - */ - public override get text(): '{' { - return LeftCurlyBrace.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `left-curly-brace${this.range}`; - } -} - -/** - * A token that represent a `}` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class RightCurlyBrace extends SimpleToken<'}'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '}' = '}'; - - /** - * Return text representation of the token. - */ - public override get text(): '}' { - return RightCurlyBrace.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `right-curly-brace${this.range}`; - } -} - -/** - * General curly brace token type. - */ -export type TCurlyBrace = LeftCurlyBrace | RightCurlyBrace; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/dash.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/dash.ts deleted file mode 100644 index a77b441ad26..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/dash.ts +++ /dev/null @@ -1,31 +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 { SimpleToken } from './simpleToken.js'; - -/** - * A token that represent a `-` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class Dash extends SimpleToken<'-'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '-' = '-'; - - /** - * Return text representation of the token. - */ - public override get text(): '-' { - return Dash.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `dash${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/dollarSign.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/dollarSign.ts deleted file mode 100644 index 5b81d0afda8..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/dollarSign.ts +++ /dev/null @@ -1,31 +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 { SimpleToken } from './simpleToken.js'; - -/** - * A token that represent a `$` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class DollarSign extends SimpleToken<'$'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '$' = '$'; - - /** - * Return text representation of the token. - */ - public override get text(): '$' { - return DollarSign.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `dollarSign${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/doubleQuote.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/doubleQuote.ts deleted file mode 100644 index ba041a03ed5..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/doubleQuote.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 { 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/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/exclamationMark.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/exclamationMark.ts deleted file mode 100644 index 32675fdf8a3..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/exclamationMark.ts +++ /dev/null @@ -1,31 +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 { SimpleToken } from './simpleToken.js'; - -/** - * A token that represent a `!` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class ExclamationMark extends SimpleToken<'!'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '!' = '!'; - - /** - * Return text representation of the token. - */ - public override get text(): '!' { - return ExclamationMark.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `exclamation-mark${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/formFeed.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/formFeed.ts deleted file mode 100644 index df5b8b0d446..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/formFeed.ts +++ /dev/null @@ -1,31 +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 { SimpleToken } from './simpleToken.js'; - -/** - * Token that represent a `form feed` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class FormFeed extends SimpleToken<'\f'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '\f' = '\f'; - - /** - * Return text representation of the token. - */ - public override get text(): '\f' { - return FormFeed.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `formfeed${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/hash.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/hash.ts deleted file mode 100644 index f499085859d..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/hash.ts +++ /dev/null @@ -1,31 +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 { SimpleToken } from './simpleToken.js'; - -/** - * A token that represent a `#` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class Hash extends SimpleToken<'#'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '#' = '#'; - - /** - * Return text representation of the token. - */ - public override get text(): '#' { - return Hash.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `hash${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/parentheses.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/parentheses.ts deleted file mode 100644 index b7ef4f5bc2a..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/parentheses.ts +++ /dev/null @@ -1,61 +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 { SimpleToken } from './simpleToken.js'; - -/** - * A token that represent a `(` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class LeftParenthesis extends SimpleToken<'('> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '(' = '('; - - /** - * Return text representation of the token. - */ - public override get text(): '(' { - return LeftParenthesis.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `left-parenthesis${this.range}`; - } -} - -/** - * A token that represent a `)` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class RightParenthesis extends SimpleToken<')'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: ')' = ')'; - - /** - * Return text representation of the token. - */ - public override get text(): ')' { - return RightParenthesis.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `right-parenthesis${this.range}`; - } -} - -/** - * General parenthesis token type. - */ -export type TParenthesis = LeftParenthesis | RightParenthesis; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/quote.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/quote.ts deleted file mode 100644 index 3c262f67b49..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/quote.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 { 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/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/simpleToken.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/simpleToken.ts deleted file mode 100644 index 5a611bb1ddd..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/simpleToken.ts +++ /dev/null @@ -1,59 +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 { Range } from '../../../../../../../../../editor/common/core/range.js'; -import { BaseToken } from '../../baseToken.js'; -import { Line } from '../../linesCodec/tokens/line.js'; - -/** - * Interface for a class that can be instantiated into a {@link SimpleToken}. - */ -export interface ISimpleTokenClass> { - /** - * Character representing the token. - */ - readonly symbol: string; - - /** - * Constructor for the token. - */ - new(...args: any[]): TSimpleToken; -} - -/** - * Base class for all "simple" tokens with a `range`. - * A simple token is the one that represents a single character. - */ -export abstract class SimpleToken extends BaseToken { - /** - * The underlying symbol of the token. - */ - public static readonly symbol: string; - - /** - * Create new token instance with range inside - * the given `Line` at the given `column number`. - */ - public static newOnLine>( - line: Line, - atColumnNumber: number, - Constructor: ISimpleTokenClass, - ): TSimpleToken { - const { range } = line; - - return new Constructor(new Range( - range.startLineNumber, - atColumnNumber, - range.startLineNumber, - atColumnNumber + Constructor.symbol.length, - )); - } -} - -/** - * Base class for all tokens that represent some form of - * a spacing character, e.g. 'space', 'tab', etc. - */ -export abstract class SpacingToken extends SimpleToken { } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/slash.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/slash.ts deleted file mode 100644 index 04d382bc89f..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/slash.ts +++ /dev/null @@ -1,31 +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 { SimpleToken } from './simpleToken.js'; - -/** - * A token that represent a `/` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class Slash extends SimpleToken<'/'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '/' = '/'; - - /** - * Return text representation of the token. - */ - public override get text(): '/' { - return Slash.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `slash${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/space.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/space.ts deleted file mode 100644 index 07eed9f58b2..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/space.ts +++ /dev/null @@ -1,31 +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 { SpacingToken } from './simpleToken.js'; - -/** - * A token that represent a `space` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class Space extends SpacingToken<' '> { - /** - * The underlying symbol of the `Space` token. - */ - public static override readonly symbol: ' ' = ' '; - - /** - * Return text representation of the token. - */ - public override get text(): ' ' { - return Space.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `space${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/tab.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/tab.ts deleted file mode 100644 index 718a01df813..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/tab.ts +++ /dev/null @@ -1,31 +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 { SpacingToken } from './simpleToken.js'; - -/** - * A token that represent a `tab` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class Tab extends SpacingToken<'\t'> { - /** - * The underlying symbol of the token. - */ - public static override readonly symbol: '\t' = '\t'; - - /** - * Return text representation of the token. - */ - public override get text(): '\t' { - return Tab.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `tab${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/tokens.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/tokens.ts deleted file mode 100644 index ee25fb83ddf..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/tokens.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export { At } from './at.js'; -export { Tab } from './tab.js'; -export { Dash } from './dash.js'; -export { Hash } from './hash.js'; -export { Word } from './word.js'; -export { Colon } from './colon.js'; -export { Quote } from './quote.js'; -export { Slash } from './slash.js'; -export { Space } from './space.js'; -export { Comma } from './comma.js'; -export { FormFeed } from './formFeed.js'; -export { DollarSign } from './dollarSign.js'; -export { VerticalTab } from './verticalTab.js'; -export { DoubleQuote } from './doubleQuote.js'; -export { ExclamationMark } from './exclamationMark.js'; -export { SimpleToken, SpacingToken } from './simpleToken.js'; -export { type TBracket, LeftBracket, RightBracket } from './brackets.js'; -export { type TCurlyBrace, LeftCurlyBrace, RightCurlyBrace } from './curlyBraces.js'; -export { type TParenthesis, LeftParenthesis, RightParenthesis } from './parentheses.js'; -export { type TAngleBracket, LeftAngleBracket, RightAngleBracket } from './angleBrackets.js'; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/verticalTab.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/verticalTab.ts deleted file mode 100644 index 4b7b4241433..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/verticalTab.ts +++ /dev/null @@ -1,31 +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 { SpacingToken } from './simpleToken.js'; - -/** - * Token that represent a `vertical tab` with a `range`. The `range` - * value reflects the position of the token in the original data. - */ -export class VerticalTab extends SpacingToken<'\v'> { - /** - * The underlying symbol of the `VerticalTab` token. - */ - public static override readonly symbol: '\v' = '\v'; - - /** - * Return text representation of the token. - */ - public override get text(): '\v' { - return VerticalTab.symbol; - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `vtab${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/word.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/word.ts deleted file mode 100644 index bea656895fc..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/simpleCodec/tokens/word.ts +++ /dev/null @@ -1,60 +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 { BaseToken } from '../../baseToken.js'; -import { Line } from '../../linesCodec/tokens/line.js'; -import { Range } from '../../../../../../../../../editor/common/core/range.js'; - -/** - * A token that represent a word - a set of continuous - * characters without stop characters, like a `space`, - * a `tab`, or a `new line`. - */ -export class Word extends BaseToken { - constructor( - /** - * The word range. - */ - range: Range, - - /** - * The string value of the word. - */ - public readonly text: TText, - ) { - super(range); - } - - /** - * Create new `Word` token with the given `text` and the range - * inside the given `Line` at the specified `column number`. - */ - public static newOnLine( - text: string, - line: Line | number, - atColumnNumber: number, - ): Word { - const startLineNumber = (typeof line === 'number') - ? line - : line.range.startLineNumber; - - const range = new Range( - startLineNumber, atColumnNumber, - startLineNumber, atColumnNumber + text.length - ); - - return new Word( - range, - text, - ); - } - - /** - * Returns a string representation of the token. - */ - public override toString(): string { - return `word("${this.shortText()}")${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/textToken.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/textToken.ts deleted file mode 100644 index ecb4c61c94e..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/textToken.ts +++ /dev/null @@ -1,19 +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 { type BaseToken } from './baseToken.js'; -import { CompositeToken } from './compositeToken.js'; - -/** - * Tokens that represent a sequence of tokens that does not - * hold an additional meaning in the text. - */ -export class Text< - TTokens extends readonly BaseToken[] = readonly BaseToken[], -> extends CompositeToken { - public override toString(): string { - return `text(${this.shortText()})${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/utils/objectStream.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/utils/objectStream.ts deleted file mode 100644 index 3c8476c2d56..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/utils/objectStream.ts +++ /dev/null @@ -1,224 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { assertNever } from '../../../../../../../../base/common/assert.js'; -import { CancellationToken } from '../../../../../../../../base/common/cancellation.js'; -import { ObservableDisposable } from '../../../utils/observableDisposable.js'; -import { newWriteableStream, ReadableStream, WriteableStream } from '../../../../../../../../base/common/stream.js'; - - -/** - * A readable stream of provided objects. - */ -export class ObjectStream extends ObservableDisposable implements ReadableStream { - /** - * Flag that indicates whether the stream has ended. - */ - private ended: boolean = false; - - /** - * Underlying writable stream instance. - */ - private readonly stream: WriteableStream; - - /** - * Interval reference that is used to periodically send - * objects to the stream in the background. - */ - private timeoutHandle: Timeout | undefined; - - constructor( - private readonly data: Generator, - private readonly cancellationToken?: CancellationToken, - ) { - super(); - - this.stream = newWriteableStream(null); - - if (cancellationToken?.isCancellationRequested) { - this.end(); - return; - } - - // send a first batch of data immediately - this.send(true); - } - - /** - * Starts process of sending data to the stream. - * - * @param stopAfterFirstSend whether to continue sending data to the stream - * or stop sending after the first batch of data is sent instead - */ - public send( - stopAfterFirstSend: boolean = false, - ): void { - // this method can be called asynchronously by the `setTimeout` utility below, hence - // the state of the cancellation token or the stream itself might have changed by that time - if (this.cancellationToken?.isCancellationRequested || this.ended) { - this.end(); - - return; - } - - this.sendData() - .then(() => { - if (this.cancellationToken?.isCancellationRequested || this.ended) { - this.end(); - - return; - } - - if (stopAfterFirstSend === true) { - this.stopStream(); - return; - } - - this.timeoutHandle = setTimeout(this.send.bind(this)); - }) - .catch((error) => { - this.stream.error(error); - this.dispose(); - }); - } - - /** - * Stop the data sending loop. - */ - public stopStream(): this { - if (this.timeoutHandle === undefined) { - return this; - } - - clearTimeout(this.timeoutHandle); - this.timeoutHandle = undefined; - - return this; - } - - /** - * Sends a provided number of objects to the stream. - */ - private async sendData( - objectsCount: number = 25, - ): Promise { - // send up to 'objectsCount' objects at a time - while (objectsCount > 0) { - try { - const next = this.data.next(); - if (next.done || this.cancellationToken?.isCancellationRequested) { - this.end(); - - return; - } - - await this.stream.write(next.value); - objectsCount--; - } catch (error) { - this.stream.error(error); - this.dispose(); - return; - } - } - } - - /** - * Ends the stream and stops sending data objects. - */ - private end(): this { - if (this.ended) { - return this; - } - this.ended = true; - - this.stopStream(); - this.stream.end(); - return this; - } - - public pause(): void { - this.stopStream(); - this.stream.pause(); - - return; - } - - public resume(): void { - this.send(); - this.stream.resume(); - - return; - } - - public destroy(): void { - this.dispose(); - } - - public removeListener(event: string, callback: (...args: any[]) => void): void { - this.stream.removeListener(event, callback); - - return; - } - - 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: (...args: 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.send(); - - return; - } - - if (event === 'error') { - this.stream.on(event, callback); - return; - } - - if (event === 'end') { - this.stream.on(event, callback); - return; - } - - assertNever( - event, - `Unexpected event name '${event}'.`, - ); - } - - /** - * Cleanup send interval and destroy the stream. - */ - public override dispose(): void { - this.stopStream(); - this.stream.destroy(); - - super.dispose(); - } - - /** - * Create new instance of the stream from a provided array. - */ - public static fromArray( - array: T[], - cancellationToken?: CancellationToken, - ): ObjectStream { - return new ObjectStream(arrayToGenerator(array), cancellationToken); - } -} - -/** - * Create a generator out of a provided array. - */ -export function arrayToGenerator>(array: T[]): Generator { - return (function* (): Generator { - for (const item of array) { - yield item; - } - })(); -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/utils/objectStreamFromTextModel.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/utils/objectStreamFromTextModel.ts deleted file mode 100644 index 2a9b2173b5d..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/base/utils/objectStreamFromTextModel.ts +++ /dev/null @@ -1,46 +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 { ITextModel } from '../../../../../../../../editor/common/model.js'; -import { ObjectStream } from './objectStream.js'; -import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; -import { CancellationToken } from '../../../../../../../../base/common/cancellation.js'; - -/** - * Create new instance of the stream from a provided text model. - */ -export function objectStreamFromTextModel( - model: ITextModel, - cancellationToken?: CancellationToken, -): ObjectStream { - return new ObjectStream(modelToGenerator(model), cancellationToken); -} - -/** - * Create a generator out of a provided text model. - */ -function modelToGenerator(model: ITextModel): Generator { - return (function* (): Generator { - const totalLines = model.getLineCount(); - let currentLine = 1; - - while (currentLine <= totalLines) { - if (model.isDisposed()) { - return undefined; - } - - yield VSBuffer.fromString( - model.getLineContent(currentLine), - ); - if (currentLine !== totalLines) { - yield VSBuffer.fromString( - model.getEOL(), - ); - } - - currentLine++; - } - })(); -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts deleted file mode 100644 index da27950bf92..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptCodec.ts +++ /dev/null @@ -1,73 +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 { VSBuffer } from '../../../../../../base/common/buffer.js'; -import { ReadableStream } from '../../../../../../base/common/stream.js'; -import { ChatPromptDecoder, TChatPromptToken } from './chatPromptDecoder.js'; - -/** - * A codec is an object capable of encoding/decoding a stream of data transforming its messages. - * Useful for abstracting a data transfer or protocol logic on top of a stream of bytes. - * - * For instance, if protocol messages need to be transferred over `TCP` connection, a codec that - * encodes the messages into a sequence of bytes before sending it to a network socket. Likewise, - * on the other end of the connection, the same codec can decode the sequence of bytes back into - * a sequence of the protocol messages. - */ -export interface ICodec { - /** - * Encode a stream of `K`s into a stream of `T`s. - */ - encode: (value: ReadableStream) => ReadableStream; - - /** - * Decode a stream of `T`s into a stream of `K`s. - */ - decode: (value: ReadableStream) => ReadableStream; -} - - -/** - * `ChatPromptCodec` type is a `ICodec` with specific types for - * stream messages and return types of the `encode`/`decode` functions. - * @see {@link ICodec} - */ -interface IChatPromptCodec extends ICodec { - /** - * Decode a stream of `VSBuffer`s into a stream of `TChatPromptToken`s. - * - * @see {@link TChatPromptToken} - * @see {@link VSBuffer} - * @see {@link ChatPromptDecoder} - */ - decode: (value: ReadableStream) => ChatPromptDecoder; -} - -/** - * Codec that is capable to encode and decode tokens of an AI chatbot prompt message. - */ -export const ChatPromptCodec: IChatPromptCodec = Object.freeze({ - /** - * Encode a stream of `TChatPromptToken`s into a stream of `VSBuffer`s. - * - * @see {@link ReadableStream} - * @see {@link VSBuffer} - */ - encode: (_stream: ReadableStream): ReadableStream => { - throw new Error('The `encode` method is not implemented.'); - }, - - /** - * Decode a of `VSBuffer`s into a readable of `TChatPromptToken`s. - * - * @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/codecs/chatPromptDecoder.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts deleted file mode 100644 index 5ac4a138fbe..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/chatPromptDecoder.ts +++ /dev/null @@ -1,202 +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 { PromptToken } from './tokens/promptToken.js'; -import { PromptAtMention } from './tokens/promptAtMention.js'; -import { VSBuffer } from '../../../../../../base/common/buffer.js'; -import { PromptSlashCommand } from './tokens/promptSlashCommand.js'; -import { ReadableStream } from '../../../../../../base/common/stream.js'; -import { PartialPromptAtMention } from './parsers/promptAtMentionParser.js'; -import { PromptTemplateVariable } from './tokens/promptTemplateVariable.js'; -import { assert, assertNever } from '../../../../../../base/common/assert.js'; -import { PartialPromptSlashCommand } from './parsers/promptSlashCommandParser.js'; -import { BaseDecoder } from './base/baseDecoder.js'; -import { PromptVariable, PromptVariableWithData } from './tokens/promptVariable.js'; -import { At } from './base/simpleCodec/tokens/at.js'; -import { Hash } from './base/simpleCodec/tokens/hash.js'; -import { Slash } from './base/simpleCodec/tokens/slash.js'; -import { DollarSign } from './base/simpleCodec/tokens/dollarSign.js'; -import { PartialPromptVariableName, PartialPromptVariableWithData } from './parsers/promptVariableParser.js'; -import { MarkdownDecoder, TMarkdownToken } from './base/markdownCodec/markdownDecoder.js'; -import { PartialPromptTemplateVariable, PartialPromptTemplateVariableStart, TPromptTemplateVariableParser } from './parsers/promptTemplateVariableParser.js'; - -/** - * Tokens produced by this decoder. - */ -export type TChatPromptToken = TMarkdownToken | (PromptVariable | PromptVariableWithData) - | PromptAtMention | PromptSlashCommand | PromptTemplateVariable; - -/** - * Decoder for the common chatbot prompt message syntax. - * For instance, the file references `#file:./path/file.md` are handled by this decoder. - */ -export class ChatPromptDecoder extends BaseDecoder { - /** - * Currently active parser object that is used to parse a well-known sequence of - * tokens, for instance, a `#file:/path/to/file.md` link that consists of `hash`, - * `word`, and `colon` tokens sequence plus the `file path` part that follows. - */ - private current?: (PartialPromptVariableName | PartialPromptVariableWithData) - | PartialPromptAtMention | PartialPromptSlashCommand - | TPromptTemplateVariableParser; - - constructor( - stream: ReadableStream, - ) { - super(new MarkdownDecoder(stream)); - } - - protected override onStreamData(token: TMarkdownToken): void { - // prompt `#variables` always start with the `#` character, hence - // initiate a parser object if we encounter respective token and - // there is no active parser object present at the moment - if ((token instanceof Hash) && !this.current) { - this.current = new PartialPromptVariableName(token); - - return; - } - - // prompt `@mentions` always start with the `@` character, hence - // initiate a parser object if we encounter respective token and - // there is no active parser object present at the moment - if ((token instanceof At) && !this.current) { - this.current = new PartialPromptAtMention(token); - - return; - } - - // prompt `/commands` always start with the `/` character, hence - // initiate a parser object if we encounter respective token and - // there is no active parser object present at the moment - if ((token instanceof Slash) && !this.current) { - this.current = new PartialPromptSlashCommand(token); - - return; - } - - // prompt `${template:variables}` always start with the `$` character, - // hence initiate a parser object if we encounter respective token and - // there is no active parser object present at the moment - if ((token instanceof DollarSign) && !this.current) { - this.current = new PartialPromptTemplateVariableStart(token); - - return; - } - - // if current parser was not yet initiated, - we are in the general "text" - // parsing mode, therefore re-emit the token immediately and continue - if (!this.current) { - this._onData.fire(token); - return; - } - - // if there is a current parser object, submit the token to it - // so it can progress with parsing the tokens sequence - const parseResult = this.current.accept(token); - - // process the parse result next - switch (parseResult.result) { - // in the case of success there might be 2 cases: - // 1) parsing fully completed and an instance of `PromptToken` is returned back, - // in this case, emit the parsed token (e.g., a `link`) and reset the current - // parser object reference so a new parsing process can be initiated next - // 2) parsing is still in progress and the next parser object is returned, hence - // we need to replace the current parser object with a new one and continue - case 'success': { - const { nextParser } = parseResult; - - if (nextParser instanceof PromptToken) { - this._onData.fire(nextParser); - delete this.current; - } else { - this.current = nextParser; - } - - break; - } - // in the case of failure, reset the current parser object - case 'failure': { - // if failed to parse a sequence of a tokens, re-emit the tokens accumulated - // so far then reset the current parser object - this.reEmitCurrentTokens(); - break; - } - } - - // if token was not consumed by the parser, call `onStreamData` again - // so the token is properly handled by the decoder in the case when a - // new sequence starts with this token - if (!parseResult.wasTokenConsumed) { - this.onStreamData(token); - } - } - - protected override onStreamEnd(): void { - try { - // if there is no currently active parser object present, nothing to do - if (this.current === undefined) { - return; - } - - // otherwise try to convert unfinished parser object to a token - - if (this.current instanceof PartialPromptVariableName) { - this._onData.fire(this.current.asPromptVariable()); - return; - } - - if (this.current instanceof PartialPromptVariableWithData) { - this._onData.fire(this.current.asPromptVariableWithData()); - return; - } - - if (this.current instanceof PartialPromptAtMention) { - this._onData.fire(this.current.asPromptAtMention()); - return; - } - - if (this.current instanceof PartialPromptSlashCommand) { - this._onData.fire(this.current.asPromptSlashCommand()); - return; - } - - assert( - (this.current instanceof PartialPromptTemplateVariableStart) === false, - 'Incomplete template variable token.', - ); - - if (this.current instanceof PartialPromptTemplateVariable) { - this._onData.fire(this.current.asPromptTemplateVariable()); - return; - } - - assertNever( - this.current, - `Unknown parser object '${this.current}'`, - ); - } catch (_error) { - // if failed to convert current parser object to a token, - // re-emit the tokens accumulated so far - this.reEmitCurrentTokens(); - } finally { - delete this.current; - super.onStreamEnd(); - } - } - - /** - * Re-emit tokens accumulated so far in the current parser object. - */ - protected reEmitCurrentTokens(): void { - if (this.current === undefined) { - return; - } - - for (const token of this.current.tokens) { - this._onData.fire(token); - } - delete this.current; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptAtMentionParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptAtMentionParser.ts deleted file mode 100644 index 2173ba438ad..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptAtMentionParser.ts +++ /dev/null @@ -1,121 +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 { PromptAtMention } from '../tokens/promptAtMention.js'; -import { assert } from '../../../../../../../base/common/assert.js'; -import { Range } from '../../../../../../../editor/common/core/range.js'; -import { BaseToken } from '../base/baseToken.js'; -import { At } from '../base/simpleCodec/tokens/at.js'; -import { Tab } from '../base/simpleCodec/tokens/tab.js'; -import { Hash } from '../base/simpleCodec/tokens/hash.js'; -import { Space } from '../base/simpleCodec/tokens/space.js'; -import { Colon } from '../base/simpleCodec/tokens/colon.js'; -import { NewLine } from '../base/linesCodec/tokens/newLine.js'; -import { FormFeed } from '../base/simpleCodec/tokens/formFeed.js'; -import { VerticalTab } from '../base/simpleCodec/tokens/verticalTab.js'; -import { TSimpleDecoderToken } from '../base/simpleCodec/simpleDecoder.js'; -import { CarriageReturn } from '../base/linesCodec/tokens/carriageReturn.js'; -import { ExclamationMark } from '../base/simpleCodec/tokens/exclamationMark.js'; -import { LeftBracket, RightBracket } from '../base/simpleCodec/tokens/brackets.js'; -import { LeftAngleBracket, RightAngleBracket } from '../base/simpleCodec/tokens/angleBrackets.js'; -import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../base/simpleCodec/parserBase.js'; - -/** - * List of characters that terminate the prompt at-mention sequence. - */ -export const STOP_CHARACTERS: readonly string[] = [Space, Tab, NewLine, CarriageReturn, VerticalTab, FormFeed, At, Colon, Hash] - .map((token) => { return token.symbol; }); - -/** - * List of characters that cannot be in an at-mention name (excluding the {@link STOP_CHARACTERS}). - */ -export const INVALID_NAME_CHARACTERS: readonly string[] = [ExclamationMark, LeftAngleBracket, RightAngleBracket, LeftBracket, RightBracket] - .map((token) => { return token.symbol; }); - -/** - * The parser responsible for parsing a `prompt @mention` sequences. - * E.g., `@workspace` or `@github` participant mention. - */ -export class PartialPromptAtMention extends ParserBase { - constructor(token: At) { - super([token]); - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - // if a `stop` character is encountered, finish the parsing process - if (STOP_CHARACTERS.includes(token.text)) { - try { - // if it is possible to convert current parser to `PromptAtMention`, return success result - return { - result: 'success', - nextParser: this.asPromptAtMention(), - wasTokenConsumed: false, - }; - } catch (error) { - // otherwise fail - return { - result: 'failure', - wasTokenConsumed: false, - }; - } finally { - // in any case this is an end of the parsing process - this.isConsumed = true; - } - } - - // variables cannot have {@link INVALID_NAME_CHARACTERS} in their names - if (INVALID_NAME_CHARACTERS.includes(token.text)) { - this.isConsumed = true; - - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - // otherwise it is a valid name character, so add it to the list of - // the current tokens and continue the parsing process - this.currentTokens.push(token); - - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } - - /** - * Try to convert current parser instance into a fully-parsed {@link PromptAtMention} token. - * - * @throws if sequence of tokens received so far do not constitute a valid prompt variable, - * for instance, if there is only `1` starting `@` token is available. - */ - public asPromptAtMention(): PromptAtMention { - // if there is only one token before the stop character - // must be the starting `@` one), then fail - assert( - this.currentTokens.length > 1, - 'Cannot create a prompt @mention out of incomplete token sequence.', - ); - - const firstToken = this.currentTokens[0]; - const lastToken = this.currentTokens[this.currentTokens.length - 1]; - - // render the characters above into strings, excluding the starting `@` character - const nameTokens = this.currentTokens.slice(1); - const atMentionName = BaseToken.render(nameTokens); - - return new PromptAtMention( - new Range( - firstToken.range.startLineNumber, - firstToken.range.startColumn, - lastToken.range.endLineNumber, - lastToken.range.endColumn, - ), - atMentionName, - ); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptSlashCommandParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptSlashCommandParser.ts deleted file mode 100644 index 557f5e74379..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptSlashCommandParser.ts +++ /dev/null @@ -1,122 +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 { assert } from '../../../../../../../base/common/assert.js'; -import { PromptSlashCommand } from '../tokens/promptSlashCommand.js'; -import { Range } from '../../../../../../../editor/common/core/range.js'; -import { BaseToken } from '../base/baseToken.js'; -import { At } from '../base/simpleCodec/tokens/at.js'; -import { Tab } from '../base/simpleCodec/tokens/tab.js'; -import { Hash } from '../base/simpleCodec/tokens/hash.js'; -import { Slash } from '../base/simpleCodec/tokens/slash.js'; -import { Space } from '../base/simpleCodec/tokens/space.js'; -import { Colon } from '../base/simpleCodec/tokens/colon.js'; -import { NewLine } from '../base/linesCodec/tokens/newLine.js'; -import { FormFeed } from '../base/simpleCodec/tokens/formFeed.js'; -import { VerticalTab } from '../base/simpleCodec/tokens/verticalTab.js'; -import { TSimpleDecoderToken } from '../base/simpleCodec/simpleDecoder.js'; -import { CarriageReturn } from '../base/linesCodec/tokens/carriageReturn.js'; -import { ExclamationMark } from '../base/simpleCodec/tokens/exclamationMark.js'; -import { LeftBracket, RightBracket } from '../base/simpleCodec/tokens/brackets.js'; -import { LeftAngleBracket, RightAngleBracket } from '../base/simpleCodec/tokens/angleBrackets.js'; -import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../base/simpleCodec/parserBase.js'; - -/** - * List of characters that terminate the prompt at-mention sequence. - */ -export const STOP_CHARACTERS: readonly string[] = [Space, Tab, NewLine, CarriageReturn, VerticalTab, FormFeed, Colon, At, Hash, Slash] - .map((token) => { return token.symbol; }); - -/** - * List of characters that cannot be in an at-mention name (excluding the {@link STOP_CHARACTERS}). - */ -export const INVALID_NAME_CHARACTERS: readonly string[] = [ExclamationMark, LeftAngleBracket, RightAngleBracket, LeftBracket, RightBracket] - .map((token) => { return token.symbol; }); - -/** - * The parser responsible for parsing a `prompt /command` sequences. - * E.g., `/search` or `/explain` command. - */ -export class PartialPromptSlashCommand extends ParserBase { - constructor(token: Slash) { - super([token]); - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - // if a `stop` character is encountered, finish the parsing process - if (STOP_CHARACTERS.includes(token.text)) { - try { - // if it is possible to convert current parser to `PromptSlashCommand`, return success result - return { - result: 'success', - nextParser: this.asPromptSlashCommand(), - wasTokenConsumed: false, - }; - } catch (error) { - // otherwise fail - return { - result: 'failure', - wasTokenConsumed: false, - }; - } finally { - // in any case this is an end of the parsing process - this.isConsumed = true; - } - } - - // variables cannot have {@link INVALID_NAME_CHARACTERS} in their names - if (INVALID_NAME_CHARACTERS.includes(token.text)) { - this.isConsumed = true; - - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - // otherwise it is a valid name character, so add it to the list of - // the current tokens and continue the parsing process - this.currentTokens.push(token); - - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } - - /** - * Try to convert current parser instance into a fully-parsed {@link PromptSlashCommand} token. - * - * @throws if sequence of tokens received so far do not constitute a valid prompt variable, - * for instance, if there is only `1` starting `/` token is available. - */ - public asPromptSlashCommand(): PromptSlashCommand { - // if there is only one token before the stop character - // must be the starting `/` one), then fail - assert( - this.currentTokens.length > 1, - 'Cannot create a prompt /command out of incomplete token sequence.', - ); - - const firstToken = this.currentTokens[0]; - const lastToken = this.currentTokens[this.currentTokens.length - 1]; - - // render the characters above into strings, excluding the starting `/` character - const nameTokens = this.currentTokens.slice(1); - const atMentionName = BaseToken.render(nameTokens); - - return new PromptSlashCommand( - new Range( - firstToken.range.startLineNumber, - firstToken.range.startColumn, - lastToken.range.endLineNumber, - lastToken.range.endColumn, - ), - atMentionName, - ); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptTemplateVariableParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptTemplateVariableParser.ts deleted file mode 100644 index b97ad0fd479..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptTemplateVariableParser.ts +++ /dev/null @@ -1,148 +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 { assert } from '../../../../../../../base/common/assert.js'; -import { PromptTemplateVariable } from '../tokens/promptTemplateVariable.js'; -import { BaseToken } from '../base/baseToken.js'; -import { TSimpleDecoderToken } from '../base/simpleCodec/simpleDecoder.js'; -import { DollarSign, LeftCurlyBrace, RightCurlyBrace } from '../base/simpleCodec/tokens/tokens.js'; -import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../base/simpleCodec/parserBase.js'; - -/** - * Parsers of the `${variable}` token sequence in a prompt text. - */ -export type TPromptTemplateVariableParser = PartialPromptTemplateVariableStart | PartialPromptTemplateVariable; - -/** - * Parser that handles start sequence of a `${variable}` token sequence in - * a prompt text. Transitions to {@link PartialPromptTemplateVariable} parser - * as soon as the `${` character sequence is found. - */ -export class PartialPromptTemplateVariableStart extends ParserBase { - constructor(token: DollarSign) { - super([token]); - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - if (token instanceof LeftCurlyBrace) { - this.currentTokens.push(token); - - this.isConsumed = true; - return { - result: 'success', - nextParser: new PartialPromptTemplateVariable(this.currentTokens), - wasTokenConsumed: true, - }; - } - - return { - result: 'failure', - wasTokenConsumed: false, - }; - } -} - -/** - * Parser that handles a partial `${variable}` token sequence in a prompt text. - */ -export class PartialPromptTemplateVariable extends ParserBase { - constructor(tokens: (DollarSign | LeftCurlyBrace)[]) { - super(tokens); - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - // template variables are terminated by the `}` character - if (token instanceof RightCurlyBrace) { - this.currentTokens.push(token); - - this.isConsumed = true; - return { - result: 'success', - nextParser: this.asPromptTemplateVariable(), - wasTokenConsumed: true, - }; - } - - // otherwise it is a valid name character, so add it to the list of - // the current tokens and continue the parsing process - this.currentTokens.push(token); - - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } - - /** - * Returns a string representation of the prompt template variable - * contents, if any is present. - */ - private get contents(): string { - const contentTokens: TSimpleDecoderToken[] = []; - - // template variables are surrounded by `${}`, hence we need to have - // at least `${` plus one character for the contents to be non-empty - if (this.currentTokens.length < 3) { - return ''; - } - - // collect all tokens besides the first two (`${`) and a possible `}` at the end - for (let i = 2; i < this.currentTokens.length; i++) { - const token = this.currentTokens[i]; - const isLastToken = (i === this.currentTokens.length - 1); - - if ((token instanceof RightCurlyBrace) && (isLastToken === true)) { - break; - } - - contentTokens.push(token); - } - - return BaseToken.render(contentTokens); - } - - /** - * Try to convert current parser instance into a {@link PromptTemplateVariable} token. - * - * @throws if: - * - current tokens sequence cannot be converted to a valid template variable token - */ - public asPromptTemplateVariable(): PromptTemplateVariable { - const firstToken = this.currentTokens[0]; - const secondToken = this.currentTokens[1]; - const lastToken = this.currentTokens[this.currentTokens.length - 1]; - - // template variables are surrounded by `${}`, hence we need - // to have at least 3 tokens in the list for a valid one - assert( - this.currentTokens.length >= 3, - 'Prompt template variable should have at least 3 tokens.', - ); - - // a complete template variable must end with a `}` - assert( - lastToken instanceof RightCurlyBrace, - 'Last token is not a "}".', - ); - - // sanity checks of the first and second tokens - assert( - firstToken instanceof DollarSign, - 'First token must be a "$".', - ); - assert( - secondToken instanceof LeftCurlyBrace, - 'Second token must be a "{".', - ); - - return new PromptTemplateVariable( - BaseToken.fullRange(this.currentTokens), - this.contents, - ); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptVariableParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptVariableParser.ts deleted file mode 100644 index ef68e78ec2d..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/parsers/promptVariableParser.ts +++ /dev/null @@ -1,252 +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 { assert } from '../../../../../../../base/common/assert.js'; -import { Range } from '../../../../../../../editor/common/core/range.js'; -import { BaseToken } from '../base/baseToken.js'; -import { PromptVariable, PromptVariableWithData } from '../tokens/promptVariable.js'; -import { At } from '../base/simpleCodec/tokens/at.js'; -import { Tab } from '../base/simpleCodec/tokens/tab.js'; -import { Hash } from '../base/simpleCodec/tokens/hash.js'; -import { Space } from '../base/simpleCodec/tokens/space.js'; -import { Colon } from '../base/simpleCodec/tokens/colon.js'; -import { NewLine } from '../base/linesCodec/tokens/newLine.js'; -import { FormFeed } from '../base/simpleCodec/tokens/formFeed.js'; -import { VerticalTab } from '../base/simpleCodec/tokens/verticalTab.js'; -import { TSimpleDecoderToken } from '../base/simpleCodec/simpleDecoder.js'; -import { CarriageReturn } from '../base/linesCodec/tokens/carriageReturn.js'; -import { ExclamationMark } from '../base/simpleCodec/tokens/exclamationMark.js'; -import { LeftBracket, RightBracket } from '../base/simpleCodec/tokens/brackets.js'; -import { LeftAngleBracket, RightAngleBracket } from '../base/simpleCodec/tokens/angleBrackets.js'; -import { assertNotConsumed, ParserBase, TAcceptTokenResult } from '../base/simpleCodec/parserBase.js'; - -/** - * List of characters that terminate the prompt variable sequence. - */ -export const STOP_CHARACTERS: readonly string[] = [Space, Tab, NewLine, CarriageReturn, VerticalTab, FormFeed, Hash, At] - .map((token) => { return token.symbol; }); - -/** - * List of characters that cannot be in a variable name (excluding the {@link STOP_CHARACTERS}). - */ -export const INVALID_NAME_CHARACTERS: readonly string[] = [Hash, Colon, ExclamationMark, LeftAngleBracket, RightAngleBracket, LeftBracket, RightBracket] - .map((token) => { return token.symbol; }); - -/** - * The parser responsible for parsing a `prompt variable name`. - * E.g., `#selection` or `#codebase` variable. If the `:` character follows - * the variable name, the parser transitions to {@link PartialPromptVariableWithData} - * that is also able to parse the `data` part of the variable. E.g., the `#file` part - * of the `#file:/path/to/something.md` sequence. - */ -export class PartialPromptVariableName extends ParserBase { - constructor(token: Hash) { - super([token]); - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - // if a `stop` character is encountered, finish the parsing process - if (STOP_CHARACTERS.includes(token.text)) { - try { - // if it is possible to convert current parser to `PromptVariable`, return success result - return { - result: 'success', - nextParser: this.asPromptVariable(), - wasTokenConsumed: false, - }; - } catch (error) { - // otherwise fail - return { - result: 'failure', - wasTokenConsumed: false, - }; - } finally { - // in any case this is an end of the parsing process - this.isConsumed = true; - } - } - - // if a `:` character is encountered, we might transition to {@link PartialPromptVariableWithData} - if (token instanceof Colon) { - this.isConsumed = true; - - // if there is only one token before the `:` character, it must be the starting - // `#` symbol, therefore fail because there is no variable name present - if (this.currentTokens.length <= 1) { - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - // otherwise, if there are more characters after `#` available, - // we have a variable name, so we can transition to {@link PromptVariableWithData} - return { - result: 'success', - nextParser: new PartialPromptVariableWithData([...this.currentTokens, token]), - wasTokenConsumed: true, - }; - } - - // variables cannot have {@link INVALID_NAME_CHARACTERS} in their names - if (INVALID_NAME_CHARACTERS.includes(token.text)) { - this.isConsumed = true; - - return { - result: 'failure', - wasTokenConsumed: false, - }; - } - - // otherwise, a valid name character, so add it to the list of - // the current tokens and continue the parsing process - this.currentTokens.push(token); - - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } - - /** - * Try to convert current parser instance into a fully-parsed {@link PromptVariable} token. - * - * @throws if sequence of tokens received so far do not constitute a valid prompt variable, - * for instance, if there is only `1` starting `#` token is available. - */ - public asPromptVariable(): PromptVariable { - // if there is only one token before the stop character - // must be the starting `#` one), then fail - assert( - this.currentTokens.length > 1, - 'Cannot create a prompt variable out of incomplete token sequence.', - ); - - const firstToken = this.currentTokens[0]; - const lastToken = this.currentTokens[this.currentTokens.length - 1]; - - // render the characters above into strings, excluding the starting `#` character - const variableNameTokens = this.currentTokens.slice(1); - const variableName = BaseToken.render(variableNameTokens); - - return new PromptVariable( - new Range( - firstToken.range.startLineNumber, - firstToken.range.startColumn, - lastToken.range.endLineNumber, - lastToken.range.endColumn, - ), - variableName, - ); - } -} - -/** - * The parser responsible for parsing a `prompt variable name` with `data`. - * E.g., the `/path/to/something.md` part of the `#file:/path/to/something.md` sequence. - */ -export class PartialPromptVariableWithData extends ParserBase { - - constructor(tokens: readonly TSimpleDecoderToken[]) { - const firstToken = tokens[0]; - const lastToken = tokens[tokens.length - 1]; - - // sanity checks of our expectations about the tokens list - assert( - tokens.length > 2, - `Tokens list must contain at least 3 items, got '${tokens.length}'.`, - ); - assert( - firstToken instanceof Hash, - `The first token must be a '#', got '${firstToken} '.`, - ); - assert( - lastToken instanceof Colon, - `The last token must be a ':', got '${lastToken} '.`, - ); - - super([...tokens]); - } - - @assertNotConsumed - public accept(token: TSimpleDecoderToken): TAcceptTokenResult { - // if a `stop` character is encountered, finish the parsing process - if (STOP_CHARACTERS.includes(token.text)) { - // in any case, success of failure below, this is an end of the parsing process - this.isConsumed = true; - - const firstToken = this.currentTokens[0]; - const lastToken = this.currentTokens[this.currentTokens.length - 1]; - - // tokens representing variable name without the `#` character at the start and - // the `:` data separator character at the end - const variableNameTokens = this.currentTokens.slice(1, this.startTokensCount - 1); - // tokens representing variable data without the `:` separator character at the start - const variableDataTokens = this.currentTokens.slice(this.startTokensCount); - // compute the full range of the variable token - const fullRange = new Range( - firstToken.range.startLineNumber, - firstToken.range.startColumn, - lastToken.range.endLineNumber, - lastToken.range.endColumn, - ); - - // render the characters above into strings - const variableName = BaseToken.render(variableNameTokens); - const variableData = BaseToken.render(variableDataTokens); - - return { - result: 'success', - nextParser: new PromptVariableWithData( - fullRange, - variableName, - variableData, - ), - wasTokenConsumed: false, - }; - } - - // otherwise, token is a valid data character - the data can contain almost any character, - // including `:` and `#`, hence add it to the list of the current tokens and continue - this.currentTokens.push(token); - - return { - result: 'success', - nextParser: this, - wasTokenConsumed: true, - }; - } - - /** - * Try to convert current parser instance into a fully-parsed {@link asPromptVariableWithData} token. - */ - public asPromptVariableWithData(): PromptVariableWithData { - // tokens representing variable name without the `#` character at the start and - // the `:` data separator character at the end - const variableNameTokens = this.currentTokens.slice(1, this.startTokensCount - 1); - // tokens representing variable data without the `:` separator character at the start - const variableDataTokens = this.currentTokens.slice(this.startTokensCount); - - // render the characters above into strings - const variableName = BaseToken.render(variableNameTokens); - const variableData = BaseToken.render(variableDataTokens); - - const firstToken = this.currentTokens[0]; - const lastToken = this.currentTokens[this.currentTokens.length - 1]; - - return new PromptVariableWithData( - new Range( - firstToken.range.startLineNumber, - firstToken.range.startColumn, - lastToken.range.endLineNumber, - lastToken.range.endColumn, - ), - variableName, - variableData, - ); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts deleted file mode 100644 index 78c4f7cb6ea..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/fileReference.ts +++ /dev/null @@ -1,50 +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 { PromptVariableWithData } from './promptVariable.js'; -import { assert } from '../../../../../../../base/common/assert.js'; -import { IRange, Range } from '../../../../../../../editor/common/core/range.js'; - -/** - * Name of the variable. - */ -const VARIABLE_NAME: string = 'file'; - -/** - * Object represents a file reference token inside a chatbot prompt. - */ -export class FileReference extends PromptVariableWithData { - constructor( - range: Range, - public readonly path: string, - ) { - super(range, VARIABLE_NAME, path); - } - - /** - * Create a {@link FileReference} from a {@link PromptVariableWithData} instance. - * @throws if variable name is not equal to {@link VARIABLE_NAME}. - */ - public static from(variable: PromptVariableWithData): FileReference { - assert( - variable.name === VARIABLE_NAME, - `Variable name must be '${VARIABLE_NAME}', got '${variable.name}'.`, - ); - - return new FileReference( - variable.range, - variable.data, - ); - } - - /** - * Get the range of the `link` part of the token (e.g., - * the `/path/to/file.md` part of `#file:/path/to/file.md`). - */ - public get linkRange(): IRange | undefined { - return super.dataRange; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptAtMention.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptAtMention.ts deleted file mode 100644 index 43bd8005235..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptAtMention.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { PromptToken } from './promptToken.js'; -import { assert } from '../../../../../../../base/common/assert.js'; -import { Range } from '../../../../../../../editor/common/core/range.js'; -import { INVALID_NAME_CHARACTERS, STOP_CHARACTERS } from '../parsers/promptVariableParser.js'; - -/** - * All prompt at-mentions start with `@` character. - */ -const START_CHARACTER: string = '@'; - -/** - * Represents a `@mention` token in a prompt text. - */ -export class PromptAtMention extends PromptToken { - constructor( - range: Range, - /** - * The name of a mention, excluding the `@` character at the start. - */ - public readonly name: string, - ) { - // sanity check of characters used in the provided mention name - for (const character of name) { - assert( - (INVALID_NAME_CHARACTERS.includes(character) === false) && - (STOP_CHARACTERS.includes(character) === false), - `Mention 'name' cannot contain character '${character}', got '${name}'.`, - ); - } - - super(range); - } - - /** - * Get full text of the token. - */ - public get text(): string { - return `${START_CHARACTER}${this.name}`; - } - - /** - * Return a string representation of the token. - */ - public override toString(): string { - return `${this.text}${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptSlashCommand.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptSlashCommand.ts deleted file mode 100644 index 1069d4a7a89..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptSlashCommand.ts +++ /dev/null @@ -1,42 +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 { PromptToken } from './promptToken.js'; -import { Range } from '../../../../../../../editor/common/core/range.js'; - -/** - * All prompt at-mentions start with `/` character. - */ -const START_CHARACTER: string = '/'; - -/** - * Represents a `/command` token in a prompt text. - */ -export class PromptSlashCommand extends PromptToken { - constructor( - range: Range, - /** - * The name of a command, excluding the `/` character at the start. - */ - public readonly name: string, - ) { - - super(range); - } - - /** - * Get full text of the token. - */ - public get text(): string { - return `${START_CHARACTER}${this.name}`; - } - - /** - * Return a string representation of the token. - */ - public override toString(): string { - return `${this.text}${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptTemplateVariable.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptTemplateVariable.ts deleted file mode 100644 index be3051f2c88..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptTemplateVariable.ts +++ /dev/null @@ -1,44 +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 { PromptToken } from './promptToken.js'; -import { Range } from '../../../../../../../editor/common/core/range.js'; -import { DollarSign } from '../base/simpleCodec/tokens/dollarSign.js'; -import { LeftCurlyBrace, RightCurlyBrace } from '../base/simpleCodec/tokens/curlyBraces.js'; - -/** - * Represents a `${variable}` token in a prompt text. - */ -export class PromptTemplateVariable extends PromptToken { - constructor( - range: Range, - /** - * The contents of the template variable, excluding - * the surrounding `${}` characters. - */ - public readonly contents: string, - ) { - super(range); - } - - /** - * Get full text of the token. - */ - public get text(): string { - return [ - DollarSign.symbol, - LeftCurlyBrace.symbol, - this.contents, - RightCurlyBrace.symbol, - ].join(''); - } - - /** - * Return a string representation of the token. - */ - public override toString(): string { - return `${this.text}${this.range}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptVariable.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptVariable.ts deleted file mode 100644 index ec5dd2e7a36..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/codecs/tokens/promptVariable.ts +++ /dev/null @@ -1,103 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { PromptToken } from './promptToken.js'; -import { IRange, Range } from '../../../../../../../editor/common/core/range.js'; - -/** - * All prompt variables start with `#` character. - */ -const START_CHARACTER: string = '#'; - -/** - * Character that separates name of a prompt variable from its data. - */ -const DATA_SEPARATOR: string = ':'; - -/** - * Represents a `#variable` token in a prompt text. - */ -export class PromptVariable extends PromptToken { - constructor( - range: Range, - /** - * The name of a prompt variable, excluding the `#` character at the start. - */ - public readonly name: string, - ) { - - super(range); - } - - /** - * Get full text of the token. - */ - public get text(): string { - return `${START_CHARACTER}${this.name}`; - } - - /** - * Return a string representation of the token. - */ - public override toString(): string { - return `${this.text}${this.range}`; - } -} - -/** - * Represents a {@link PromptVariable} with additional data token in a prompt text. - * (e.g., `#variable:/path/to/file.md`) - */ -export class PromptVariableWithData extends PromptVariable { - constructor( - fullRange: Range, - /** - * The name of the variable, excluding the starting `#` character. - */ - name: string, - - /** - * The data of the variable, excluding the starting {@link DATA_SEPARATOR} character. - */ - public readonly data: string, - ) { - super(fullRange, name); - } - - /** - * Get full text of the token. - */ - public override get text(): string { - return `${START_CHARACTER}${this.name}${DATA_SEPARATOR}${this.data}`; - } - - /** - * Range of the `data` part of the variable. - */ - public get dataRange(): IRange | undefined { - const { range } = this; - - // calculate the start column number of the `data` part of the variable - const dataStartColumn = range.startColumn + - START_CHARACTER.length + this.name.length + - DATA_SEPARATOR.length; - - // create `range` of the `data` part of the variable - const result = new Range( - range.startLineNumber, - dataStartColumn, - range.endLineNumber, - range.endColumn, - ); - - // if the resulting range is empty, return `undefined` - // because there is no `data` part present in the variable - if (result.isEmpty()) { - return undefined; - } - - return result; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 51129a130f1..9aa457af4be 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -21,7 +21,8 @@ import { IToolData } from '../languageModelToolsService.js'; import { PromptsConfig } from './config/config.js'; import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, isPromptOrInstructionsFile } from './config/promptFileLocations.js'; import { PromptsType } from './promptTypes.js'; -import { IPromptParserResult, IPromptPath, IPromptsService } from './service/promptsService.js'; +import { ParsedPromptFile } from './service/newPromptsParser.js'; +import { IPromptPath, IPromptsService } from './service/promptsService.js'; export type InstructionsCollectionEvent = { applyingInstructionsCount: number; @@ -46,7 +47,7 @@ type InstructionsCollectionClassification = { export class ComputeAutomaticInstructions { - private _parseResults: ResourceMap = new ResourceMap(); + private _parseResults: ResourceMap = new ResourceMap(); constructor( private readonly _readFileTool: IToolData | undefined, @@ -60,12 +61,12 @@ export class ComputeAutomaticInstructions { ) { } - private async _parseInstructionsFile(uri: URI, token: CancellationToken): Promise { + private async _parseInstructionsFile(uri: URI, token: CancellationToken): Promise { if (this._parseResults.has(uri)) { return this._parseResults.get(uri)!; } try { - const result = await this._promptsService.parse(uri, PromptsType.instructions, token); + const result = await this._promptsService.parseNew(uri, token); this._parseResults.set(uri, result); return result; } catch (error) { @@ -125,11 +126,7 @@ export class ComputeAutomaticInstructions { continue; } - if (parsedFile.metadata?.promptType !== PromptsType.instructions) { - this._logService.trace(`[InstructionsContextComputer] Not an instruction file: ${uri}`); - continue; - } - const applyTo = parsedFile.metadata.applyTo; + const applyTo = parsedFile.header?.applyTo; if (!applyTo) { this._logService.trace(`[InstructionsContextComputer] No 'applyTo' found: ${uri}`); @@ -262,12 +259,11 @@ export class ComputeAutomaticInstructions { const entries: string[] = []; for (const { uri } of instructionFiles) { const parsedFile = await this._parseInstructionsFile(uri, token); - if (parsedFile?.metadata?.promptType !== PromptsType.instructions) { - continue; + if (parsedFile) { + const applyTo = parsedFile.header?.applyTo ?? '**/*'; + const description = parsedFile.header?.description ?? ''; + entries.push(`| '${getFilePath(uri)}' | ${applyTo} | ${description} |`); } - const applyTo = parsedFile.metadata.applyTo ?? '**/*'; - const description = parsedFile.metadata.description ?? ''; - entries.push(`| '${getFilePath(uri)}' | ${applyTo} | ${description} |`); } if (entries.length === 0) { return entries; @@ -299,13 +295,14 @@ export class ComputeAutomaticInstructions { let next = todo.pop(); while (next) { const result = await this._parseInstructionsFile(next, token); - if (result) { + if (result && result.body) { const refsToCheck: { resource: URI }[] = []; - for (const ref of result.fileReferences) { - if (!seen.has(ref) && (isPromptOrInstructionsFile(ref) || this._workspaceService.getWorkspaceFolder(ref) !== undefined)) { + for (const ref of result.body.fileReferences) { + const url = result.body.resolveFilePath(ref.content); + if (url && !seen.has(url) && (isPromptOrInstructionsFile(url) || this._workspaceService.getWorkspaceFolder(url) !== undefined)) { // only add references that are either prompt or instruction files or are part of the workspace - refsToCheck.push({ resource: ref }); - seen.add(ref); + refsToCheck.push({ resource: url }); + seen.add(url); } } if (refsToCheck.length > 0) { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts index a8e88f5e659..4144dd7b5e7 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts @@ -5,6 +5,7 @@ import type { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { URI } from '../../../../../../base/common/uri.js'; import { PromptsType } from '../promptTypes.js'; import { INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, getPromptFileDefaultLocation } from './promptFileLocations.js'; @@ -165,54 +166,52 @@ export namespace PromptsConfig { /** * Get value of the prompt file recommendations configuration setting. + * @param configService Configuration service instance + * @param resource Optional resource URI to get workspace folder-specific settings * @see {@link PROMPT_FILES_SUGGEST_KEY}. */ - export function getPromptFilesRecommendationsValue(configService: IConfigurationService): Record | undefined { - const configValue = configService.getValue(PromptsConfig.PROMPT_FILES_SUGGEST_KEY); + export function getPromptFilesRecommendationsValue(configService: IConfigurationService, resource?: URI): Record | undefined { + // Get the merged configuration value (VS Code automatically merges all levels: default → user → workspace → folder) + const configValue = configService.getValue(PromptsConfig.PROMPT_FILES_SUGGEST_KEY, { resource }); - if (configValue === undefined || configValue === null || Array.isArray(configValue)) { + if (!configValue || typeof configValue !== 'object' || Array.isArray(configValue)) { return undefined; } - // note! this would be also true for `null` and `array`, - // but those cases are already handled above - if (typeof configValue === 'object') { - const suggestions: Record = {}; + const suggestions: Record = {}; - for (const [promptName, value] of Object.entries(configValue)) { - const cleanPromptName = promptName.trim(); + for (const [promptName, value] of Object.entries(configValue)) { + const cleanPromptName = promptName.trim(); - // Skip empty prompt names - if (!cleanPromptName) { - continue; - } - - // Accept boolean values directly - if (typeof value === 'boolean') { - suggestions[cleanPromptName] = value; - continue; - } - - // Accept string values as when clauses - if (typeof value === 'string') { - const cleanValue = value.trim(); - if (cleanValue) { - suggestions[cleanPromptName] = cleanValue; - } - continue; - } - - // Convert other truthy/falsy values to boolean - const booleanValue = asBoolean(value); - if (booleanValue !== undefined) { - suggestions[cleanPromptName] = booleanValue; - } + // Skip empty prompt names + if (!cleanPromptName) { + continue; } - return suggestions; + // Accept boolean values directly + if (typeof value === 'boolean') { + suggestions[cleanPromptName] = value; + continue; + } + + // Accept string values as when clauses + if (typeof value === 'string') { + const cleanValue = value.trim(); + if (cleanValue) { + suggestions[cleanPromptName] = cleanValue; + } + continue; + } + + // Convert other truthy/falsy values to boolean + const booleanValue = asBoolean(value); + if (booleanValue !== undefined) { + suggestions[cleanPromptName] = booleanValue; + } } - return undefined; + // Return undefined if no valid suggestions were found + return Object.keys(suggestions).length > 0 ? suggestions : undefined; } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/configMigration.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/configMigration.ts deleted file mode 100644 index 38d47d6dd1c..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/configMigration.ts +++ /dev/null @@ -1,119 +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 { assert } from '../../../../../../base/common/assert.js'; -import { ILogService } from '../../../../../../platform/log/common/log.js'; -import { asBoolean, PromptsConfig } from './config.js'; -import { IWorkbenchContribution } from '../../../../../common/contributions.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; - -/** - * Contribution that migrates the old config setting value to a new one. - * - * Note! This is a temporary logic and can be removed on ~2026-04-29. - */ -export class ConfigMigration implements IWorkbenchContribution { - constructor( - @ILogService private readonly logService: ILogService, - @IConfigurationService private readonly configService: IConfigurationService, - ) { - // migrate the old config setting value to a new one - this.migrateConfig() - .catch((error) => { - this.logService.warn('failed to migrate config setting value.', error); - }); - } - - /** - * The main function that implements the migration logic. - */ - private async migrateConfig(): Promise { - const value = await this.configService.getValue(PromptsConfig.KEY); - - // if setting is not set, nothing to do - if ((value === undefined) || (value === null)) { - return; - } - - // if the setting value is a boolean, we don't need to do - // anything since it is already a valid configuration value - if ((typeof value === 'boolean') || (asBoolean(value) !== undefined)) { - return; - } - - // in the old setting logic an array of strings was treated - // as a list of locations, so we need to migrate that - if (Array.isArray(value)) { - - // copy array values into a map of paths - const locationsValue: Record = {}; - for (const filePath of value) { - if (typeof filePath !== 'string') { - continue; - } - const trimmedValue = filePath.trim(); - if (!trimmedValue) { - continue; - } - - locationsValue[trimmedValue] = true; - } - - await this.configService.updateValue(PromptsConfig.KEY, true); - await this.configService.updateValue(PromptsConfig.PROMPT_LOCATIONS_KEY, locationsValue); - return; - } - - // in the old setting logic an object was treated as a map - // of `location -> boolean`, so we need to migrate that - if (typeof value === 'object') { - // sanity check on the contents of value variable - while - // we've handled the 'null' case above this assertion is - // here to prevent churn when this block is moved around - assert( - value !== null, - 'Object value must not be a null.', - ); - - // copy object values into a map of paths - const locationsValue: Record = {}; - for (const [location, enabled] of Object.entries(value)) { - // if the old location enabled value wasn't a boolean - // then ignore it as it is not a valid value - if ((typeof enabled !== 'boolean') || (asBoolean(enabled) === undefined)) { - continue; - } - - const trimmedValue = location.trim(); - if (!trimmedValue) { - continue; - } - - locationsValue[trimmedValue] = enabled; - } - - await this.configService.updateValue(PromptsConfig.KEY, true); - await this.configService.updateValue(PromptsConfig.PROMPT_LOCATIONS_KEY, locationsValue); - - return; - } - - // in the old setting logic a string was treated as a single - // location path, so we need to migrate that - if (typeof value === 'string') { - // sanity check on the contents of value variable - while - // we've handled the 'boolean' case above this assertion is - // here to prevent churn when this block is moved around - assert( - asBoolean(value) === undefined, - `String value must not be a boolean, got '${value}'.`, - ); - - await this.configService.updateValue(PromptsConfig.KEY, true); - await this.configService.updateValue(PromptsConfig.PROMPT_LOCATIONS_KEY, { [value]: true }); - return; - } - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts deleted file mode 100644 index 47705213282..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/filePromptContentsProvider.ts +++ /dev/null @@ -1,166 +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 { PROMPT_LANGUAGE_ID } from '../promptTypes.js'; -import { IPromptContentsProvider } from './types.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { assert } from '../../../../../../base/common/assert.js'; -import { CancellationError } from '../../../../../../base/common/errors.js'; -import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { IModelService } from '../../../../../../editor/common/services/model.js'; -import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; -import { isPromptOrInstructionsFile } from '../config/promptFileLocations.js'; -import { IPromptContentsProviderOptions, PromptContentsProviderBase } from './promptContentsProviderBase.js'; -import { OpenFailed, NotPromptFile, ResolveError, FolderReference } from '../../promptFileReferenceErrors.js'; -import { FileChangesEvent, FileChangeType, IFileService } from '../../../../../../platform/files/common/files.js'; - -/** - * Prompt contents provider for a file on the disk referenced by - * a provided {@link URI}. - */ -export class FilePromptContentProvider extends PromptContentsProviderBase implements IPromptContentsProvider { - public override get sourceName(): string { - return 'file'; - } - - public override get languageId(): string { - if (this.options.languageId) { - return this.options.languageId; - } - - const model = this.modelService.getModel(this.uri); - - if (model !== null) { - return model.getLanguageId(); - } - - const inferredId = this.languageService - .guessLanguageIdByFilepathOrFirstLine(this.uri); - - if (inferredId !== null) { - return inferredId; - } - - // fallback to the default prompt language ID - return PROMPT_LANGUAGE_ID; - } - - constructor( - public readonly uri: URI, - options: IPromptContentsProviderOptions, - @IFileService private readonly fileService: IFileService, - @IModelService private readonly modelService: IModelService, - @ILanguageService private readonly languageService: ILanguageService, - ) { - super(options); - - if (options.updateOnChange) { - // make sure the object is updated on file changes - this._register( - this.fileService.onDidFilesChange((event) => { - // if file was added or updated, forward the event to - // the `getContentsStream()` produce a new stream for file contents - if (event.contains(this.uri, FileChangeType.ADDED, FileChangeType.UPDATED)) { - // we support only full file parsing right now because - // the event doesn't contain a list of changed lines - this.onChangeEmitter.fire('full'); - return; - } - - // if file was deleted, forward the event to - // the `getContentsStream()` produce an error - if (event.contains(this.uri, FileChangeType.DELETED)) { - this.onChangeEmitter.fire(event); - return; - } - }), - ); - } - } - - /** - * Creates a stream of lines from the file based on the changes listed in - * the provided event. - * - * @param event - event that describes the changes in the file; `'full'` is - * the special value that means that all contents have changed - * @param cancellationToken - token that cancels this operation - */ - protected async getContentsStream( - _event: FileChangesEvent | 'full', - cancellationToken?: CancellationToken, - ): Promise { - assert( - !cancellationToken?.isCancellationRequested, - new CancellationError(), - ); - - // get the binary stream of the file contents - let fileStream; - try { - // ensure that the referenced URI points to a file before - // trying to get a stream for its contents - const info = await this.fileService.resolve(this.uri); - - // validate that the cancellation was not yet requested - assert( - !cancellationToken?.isCancellationRequested, - new CancellationError(), - ); - - assert( - info.isFile, - new FolderReference(this.uri), - ); - - const { allowNonPromptFiles } = this.options; - - // if URI doesn't point to a prompt file, don't try to resolve it, - // unless the `allowNonPromptFiles` option is set to `true` - if ((allowNonPromptFiles !== true) && (isPromptOrInstructionsFile(this.uri) === false)) { - throw new NotPromptFile(this.uri); - } - - fileStream = await this.fileService.readFileStream(this.uri); - - // after the promise above complete, this object can be already disposed or - // the cancellation could be requested, in that case destroy the stream and - // throw cancellation error - if (this.isDisposed || cancellationToken?.isCancellationRequested) { - fileStream.value.destroy(); - throw new CancellationError(); - } - - return fileStream.value; - } catch (error) { - if ((error instanceof ResolveError) || (error instanceof CancellationError)) { - throw error; - } - - throw new OpenFailed(this.uri, error); - } - } - - public override createNew( - promptContentsSource: { uri: URI }, - options: IPromptContentsProviderOptions, - ): IPromptContentsProvider { - return new FilePromptContentProvider( - promptContentsSource.uri, - options, - this.fileService, - this.modelService, - this.languageService, - ); - } - - /** - * String representation of this object. - */ - public override toString(): string { - return `file-prompt-contents-provider:${this.uri.path}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts deleted file mode 100644 index 7c9e58dc3d3..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/promptContentsProviderBase.ts +++ /dev/null @@ -1,196 +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 { assert } from '../../../../../../base/common/assert.js'; -import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { cancelPreviousCalls } from '../../../../../../base/common/decorators/cancelPreviousCalls.js'; -import { CancellationError } from '../../../../../../base/common/errors.js'; -import { Emitter } from '../../../../../../base/common/event.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { FailedToResolveContentsStream, ResolveError } from '../../promptFileReferenceErrors.js'; -import { INSTRUCTIONS_LANGUAGE_ID, MODE_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType } from '../promptTypes.js'; -import { ObservableDisposable } from '../utils/observableDisposable.js'; -import { IPromptContentsProvider } from './types.js'; - -/** - * Options of the {@link PromptContentsProviderBase} class. - */ -export interface IPromptContentsProviderOptions { - /** - * Whether to allow files that don't have usual prompt - * file extension to be treated as a prompt file. - */ - readonly allowNonPromptFiles: boolean; - - /** - * Language ID to use for the prompt contents. If not set, the language ID will be inferred from the file. - */ - readonly languageId: string | undefined; - - /** - * If set to `true`, the contents provider will listen for updates and retrigger a parse. - */ - readonly updateOnChange: boolean; -} - - -/** - * Base class for prompt contents providers. Classes that extend this one are responsible to: - * - * - implement the {@link getContentsStream} method to provide the contents stream - * of a prompt; this method should throw a `ResolveError` or its derivative if the contents - * cannot be parsed for any reason - * - fire a {@link TChangeEvent} event on the {@link onChangeEmitter} event when - * prompt contents change - * - misc: - * - provide the {@link uri} property that represents the URI of a prompt that - * the contents are for - * - implement the {@link toString} method to return a string representation of this - * provider type to aid with debugging/tracing - */ -export abstract class PromptContentsProviderBase< - TChangeEvent extends NonNullable, -> extends ObservableDisposable implements IPromptContentsProvider { - public abstract readonly uri: URI; - public abstract createNew(promptContentsSource: { uri: URI }, options: IPromptContentsProviderOptions): IPromptContentsProvider; - public abstract override toString(): string; - public abstract get languageId(): string; - public abstract get sourceName(): string; - - /** - * Prompt contents stream. - */ - public get contents(): Promise { - return this.getContentsStream('full'); - } - - /** - * Prompt type used to determine how to interpret file contents. - */ - public get promptType(): PromptsType | 'non-prompt' { - const { languageId } = this; - - if (languageId === PROMPT_LANGUAGE_ID) { - return PromptsType.prompt; - } - - if (languageId === INSTRUCTIONS_LANGUAGE_ID) { - return PromptsType.instructions; - } - - if (languageId === MODE_LANGUAGE_ID) { - return PromptsType.mode; - } - - return 'non-prompt'; - } - - /** - * Function to get contents stream for the provider. This function should - * throw a `ResolveError` or its derivative if the contents cannot be parsed. - * - * @param changesEvent The event that triggered the change. The special - * `'full'` value means that everything has changed hence entire prompt - * contents need to be re-parsed from scratch. - */ - protected abstract getContentsStream( - changesEvent: TChangeEvent | 'full', - cancellationToken?: CancellationToken, - ): Promise; - - /** - * Internal event emitter for the prompt contents change event. Classes that extend - * this abstract class are responsible to use this emitter to fire the contents change - * event when the prompt contents get modified. - */ - protected readonly onChangeEmitter = this._register(new Emitter()); - - /** - * Options passed to the constructor - */ - protected readonly options: IPromptContentsProviderOptions; - - constructor( - options: IPromptContentsProviderOptions, - ) { - super(); - - this.options = options; - } - - /** - * Event emitter for the prompt contents change event. - * See {@link onContentChanged} for more details. - */ - private readonly onContentChangedEmitter = this._register(new Emitter()); - - /** - * Event that fires when the prompt contents change. The event is either - * a `VSBufferReadableStream` stream with changed contents or an instance of - * the `ResolveError` class representing a parsing failure case. - * - * `Note!` this field is meant to be used by the external consumers of the prompt - * contents provider that the classes that extend this abstract class. - * Please use the {@link onChangeEmitter} event to provide a change - * event in your prompt contents implementation instead. - */ - public readonly onContentChanged = this.onContentChangedEmitter.event; - - /** - * Internal common implementation of the event that should be fired when - * prompt contents change. - */ - @cancelPreviousCalls - private onContentsChanged( - event: TChangeEvent | 'full', - cancellationToken?: CancellationToken, - ): this { - const promise = (cancellationToken?.isCancellationRequested) - ? Promise.reject(new CancellationError()) - : this.getContentsStream(event, cancellationToken); - - promise - .then((stream) => { - if (cancellationToken?.isCancellationRequested || this.isDisposed) { - stream.destroy(); - throw new CancellationError(); - } - - this.onContentChangedEmitter.fire(stream); - }) - .catch((error) => { - if (error instanceof ResolveError) { - this.onContentChangedEmitter.fire(error); - - return; - } - - this.onContentChangedEmitter.fire( - new FailedToResolveContentsStream(this.uri, error), - ); - }); - - return this; - } - - /** - * Start producing the prompt contents data. - */ - public start(token?: CancellationToken): this { - assert( - !this.isDisposed, - 'Cannot start contents provider that was already disposed.', - ); - - // `'full'` means "everything has changed" - this.onContentsChanged('full', token); - - // subscribe to the change event emitted by a child class - this._register(this.onChangeEmitter.event(this.onContentsChanged, this)); - - return this; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts deleted file mode 100644 index aea65908ccf..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/textModelContentsProvider.ts +++ /dev/null @@ -1,93 +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 { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { ITextModel } from '../../../../../../editor/common/model.js'; -import { TextModel } from '../../../../../../editor/common/model/textModel.js'; -import { IModelContentChangedEvent } from '../../../../../../editor/common/textModelEvents.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { objectStreamFromTextModel } from '../codecs/base/utils/objectStreamFromTextModel.js'; -import { FilePromptContentProvider } from './filePromptContentsProvider.js'; -import { IPromptContentsProviderOptions, PromptContentsProviderBase } from './promptContentsProviderBase.js'; -import { IPromptContentsProvider } from './types.js'; - -/** - * Prompt contents provider for a {@link ITextModel} instance. - */ -export class TextModelContentsProvider extends PromptContentsProviderBase { - /** - * URI component of the prompt associated with this contents provider. - */ - public get uri(): URI { - return this.model.uri; - } - - public override get sourceName(): string { - return 'text-model'; - } - - public override get languageId(): string { - return this.options.languageId ?? this.model.getLanguageId(); - } - - constructor( - private readonly model: ITextModel, - options: IPromptContentsProviderOptions, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - super(options); - - this._register(this.model.onWillDispose(this.dispose.bind(this))); - if (options.updateOnChange) { - this._register(this.model.onDidChangeContent(this.onChangeEmitter.fire.bind(this.onChangeEmitter))); - } - } - - /** - * Creates a stream of binary data from the text model based on the changes - * listed in the provided event. - * - * Note! this method implements a basic logic which does not take into account - * the `_event` argument for incremental updates. This needs to be improved. - * - * @param _event - event that describes the changes in the text model; `'full'` is - * the special value that means that all contents have changed - * @param cancellationToken - token that cancels this operation - */ - protected override async getContentsStream( - _event: IModelContentChangedEvent | 'full', - cancellationToken?: CancellationToken, - ): Promise { - return objectStreamFromTextModel(this.model, cancellationToken); - } - - public override createNew( - promptContentsSource: TextModel | { uri: URI }, - options: IPromptContentsProviderOptions, - ): IPromptContentsProvider { - if (promptContentsSource instanceof TextModel) { - return this.instantiationService.createInstance( - TextModelContentsProvider, - promptContentsSource, - options, - ); - } - - return this.instantiationService.createInstance( - FilePromptContentProvider, - promptContentsSource.uri, - options, - ); - } - - /** - * String representation of this object. - */ - public override toString(): string { - return `text-model-prompt-contents-provider:${this.uri.path}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.ts deleted file mode 100644 index a26eaa43330..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/contentProviders/types.ts +++ /dev/null @@ -1,70 +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 { URI } from '../../../../../../base/common/uri.js'; -import { Event } from '../../../../../../base/common/event.js'; -import { ResolveError } from '../../promptFileReferenceErrors.js'; -import { IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; -import { PromptsType } from '../promptTypes.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { IPromptContentsProviderOptions } from './promptContentsProviderBase.js'; - -/** - * Interface for a prompt contents provider. Prompt contents providers are - * responsible for providing contents of a prompt as a byte streams and - * allow to subscribe to the change events of the prompt contents. - */ -export interface IPromptContentsProvider extends IDisposable { - /** - * URI component of the prompt associated with this contents provider. - */ - readonly uri: URI; - - /** - * Language ID of the prompt contents. - */ - readonly languageId: string; - - /** - * Prompt type used to determine how to interpret file contents. - */ - readonly promptType: PromptsType | 'non-prompt'; - - /** - * Prompt contents stream. - */ - readonly contents: Promise; - - /** - * Prompt contents source name. - */ - readonly sourceName: string; - - /** - * Event that fires when the prompt contents change. The event is either a - * {@linkcode VSBufferReadableStream} stream with changed contents or - * an instance of the {@linkcode ResolveError} error. - */ - readonly onContentChanged: Event; - - /** - * Subscribe to `onDispose` event of the contents provider. - */ - readonly onDispose: Event; - - /** - * Start the contents provider to produce the underlying contents. - */ - start(token?: CancellationToken): this; - - /** - * Create a new instance of prompt contents provider. - */ - createNew( - promptContentsSource: { uri: URI }, - options: IPromptContentsProviderOptions, - ): IPromptContentsProvider; -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/PromptHeaderDefinitionProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/PromptHeaderDefinitionProvider.ts index 785b407da9c..0657805ac18 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/PromptHeaderDefinitionProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/PromptHeaderDefinitionProvider.ts @@ -4,71 +4,42 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { Position } from '../../../../../../editor/common/core/position.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { Definition, DefinitionProvider } from '../../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; import { IChatModeService } from '../../chatModes.js'; -import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId } from '../promptTypes.js'; +import { getPromptsTypeForLanguageId } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; -import { PromptModeMetadata } from '../parsers/promptHeader/metadata/mode.js'; -import { PromptHeader } from '../parsers/promptHeader/promptHeader.js'; -export class PromptHeaderDefinitionProvider extends Disposable implements DefinitionProvider { +export class PromptHeaderDefinitionProvider implements DefinitionProvider { /** * Debug display name for this provider. */ - public readonly _debugDisplayName: string = 'PromptHeaderHoverProvider'; + public readonly _debugDisplayName: string = 'PromptHeaderDefinitionProvider'; constructor( @IPromptsService private readonly promptsService: IPromptsService, - @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, @IChatModeService private readonly chatModeService: IChatModeService, ) { - super(); - - this._register(this.languageService.definitionProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, this)); } + async provideDefinition(model: ITextModel, position: Position, token: CancellationToken): Promise { const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); if (!promptType) { - // if the model is not a prompt, we don't provide any completions - return undefined; - } - - const parser = this.promptsService.getSyntaxParserFor(model); - await parser.start(token).settled(); - - if (token.isCancellationRequested) { + // if the model is not a prompt, we don't provide any definitions return undefined; } + const parser = this.promptsService.getParsedPromptFile(model); const header = parser.header; if (!header) { return undefined; } - const completed = await header.settled; - if (!completed || token.isCancellationRequested) { - return undefined; - } - - if (header instanceof PromptHeader) { - const mode = header.metadataUtility.mode; - if (mode?.range.containsPosition(position)) { - return this.getModeDefinition(mode, position); - } - } - return undefined; - } - - - private getModeDefinition(mode: PromptModeMetadata, position: Position): Definition | undefined { - const value = mode.value; - if (value && mode.valueRange?.containsPosition(position)) { - const mode = this.chatModeService.findModeByName(value); + const modeAttr = header.getAttribute('mode'); + if (modeAttr && modeAttr.value.type === 'string' && modeAttr.range.containsPosition(position)) { + const mode = this.chatModeService.findModeByName(modeAttr.value.value); if (mode && mode.uri) { return { uri: mode.uri.get(), diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/frontMatterDecoration.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/frontMatterDecoration.ts deleted file mode 100644 index d07720eff5b..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/frontMatterDecoration.ts +++ /dev/null @@ -1,120 +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 { Position } from '../../../../../../../../editor/common/core/position.js'; -import { localize } from '../../../../../../../../nls.js'; -import { contrastBorder, editorBackground } from '../../../../../../../../platform/theme/common/colorRegistry.js'; -import { asCssVariable, ColorIdentifier, darken, registerColor } from '../../../../../../../../platform/theme/common/colorUtils.js'; -import { BaseToken } from '../../../codecs/base/baseToken.js'; -import { FrontMatterHeader } from '../../../codecs/base/markdownExtensionsCodec/tokens/frontMatterHeader.js'; -import { CssClassModifiers } from '../types.js'; -import { FrontMatterMarkerDecoration } from './frontMatterMarkerDecoration.js'; -import { ReactiveDecorationBase } from './utils/reactiveDecorationBase.js'; -import { IReactiveDecorationClassNames, TAddAccessor, TDecorationStyles } from './utils/types.js'; - -/** - * Decoration CSS class names. - */ -export enum CssClassNames { - Main = '.prompt-front-matter-decoration', - Inline = '.prompt-front-matter-decoration-inline', - MainInactive = `${CssClassNames.Main}${CssClassModifiers.Inactive}`, - InlineInactive = `${CssClassNames.Inline}${CssClassModifiers.Inactive}`, -} - -/** - * Main background color of `active` Front Matter header block. - */ -export const BACKGROUND_COLOR: ColorIdentifier = registerColor( - 'prompt.frontMatter.background', - { dark: darken(editorBackground, 0.2), light: darken(editorBackground, 0.05), hcDark: contrastBorder, hcLight: contrastBorder }, - localize('chat.prompt.frontMatter.background.description', "Background color of a Front Matter header block."), -); - -/** - * Background color of `inactive` Front Matter header block. - */ -export const INACTIVE_BACKGROUND_COLOR: ColorIdentifier = registerColor( - 'prompt.frontMatter.inactiveBackground', - { dark: darken(editorBackground, 0.1), light: darken(editorBackground, 0.025), hcDark: contrastBorder, hcLight: contrastBorder }, - localize('chat.prompt.frontMatter.inactiveBackground.description', "Background color of an inactive Front Matter header block."), -); - -/** - * CSS styles for the decoration. - */ -export const CSS_STYLES = { - [CssClassNames.Main]: [ - `background-color: ${asCssVariable(BACKGROUND_COLOR)};`, - 'z-index: -1;', // this is required to allow for selections to appear above the decoration background - ], - [CssClassNames.MainInactive]: [ - `background-color: ${asCssVariable(INACTIVE_BACKGROUND_COLOR)};`, - ], - [CssClassNames.InlineInactive]: [ - 'color: var(--vscode-disabledForeground);', - ], - ...FrontMatterMarkerDecoration.cssStyles, -}; - -/** - * Editor decoration for the Front Matter header token inside a prompt. - */ -export class FrontMatterDecoration extends ReactiveDecorationBase { - constructor( - accessor: TAddAccessor, - token: FrontMatterHeader, - ) { - super(accessor, token); - - this.childDecorators.push( - new FrontMatterMarkerDecoration(accessor, token.startMarker), - new FrontMatterMarkerDecoration(accessor, token.endMarker), - ); - } - - public override setCursorPosition( - position: Position | null | undefined, - ): this is { readonly changed: true } { - const result = super.setCursorPosition(position); - - for (const marker of this.childDecorators) { - if ((marker instanceof FrontMatterMarkerDecoration) === false) { - continue; - } - - // activate/deactivate markers based on the active state - // of the main Front Matter header decoration - marker.activate(this.active); - } - - return result; - } - - protected override get classNames(): IReactiveDecorationClassNames { - return CssClassNames; - } - - protected override get isWholeLine(): boolean { - return true; - } - - protected override get description(): string { - return 'Front Matter header decoration.'; - } - - public static get cssStyles(): TDecorationStyles { - return CSS_STYLES; - } - - /** - * Whether current decoration class can decorate provided token. - */ - public static handles( - token: BaseToken, - ): token is FrontMatterHeader { - return token instanceof FrontMatterHeader; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/frontMatterMarkerDecoration.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/frontMatterMarkerDecoration.ts deleted file mode 100644 index 2a4a174c9f4..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/frontMatterMarkerDecoration.ts +++ /dev/null @@ -1,56 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CssClassModifiers } from '../types.js'; -import { TDecorationStyles, IReactiveDecorationClassNames } from './utils/types.js'; -import { FrontMatterMarker } from '../../../codecs/base/markdownExtensionsCodec/tokens/frontMatterMarker.js'; -import { ReactiveDecorationBase } from './utils/reactiveDecorationBase.js'; - -/** - * Decoration CSS class names. - */ -export enum CssClassNames { - Main = '.prompt-front-matter-decoration-marker', - Inline = '.prompt-front-matter-decoration-marker-inline', - MainInactive = `${CssClassNames.Main}${CssClassModifiers.Inactive}`, - InlineInactive = `${CssClassNames.Inline}${CssClassModifiers.Inactive}`, -} - -/** - * Editor decoration for a `marker` token of a Front Matter header. - */ -export class FrontMatterMarkerDecoration extends ReactiveDecorationBase { - /** - * Activate/deactivate the decoration. - */ - public activate(state: boolean): this { - const position = (state === true) - ? this.token.range.getStartPosition() - : null; - - this.setCursorPosition(position); - - return this; - } - - protected override get classNames(): IReactiveDecorationClassNames { - return CssClassNames; - } - - protected override get description(): string { - return 'Marker decoration of a Front Matter header.'; - } - - public static get cssStyles(): TDecorationStyles { - return { - [CssClassNames.Inline]: [ - 'color: var(--vscode-disabledForeground);', - ], - [CssClassNames.InlineInactive]: [ - 'opacity: 0.25;', - ], - }; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/utils/decorationBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/utils/decorationBase.ts deleted file mode 100644 index 8f28ec33247..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/utils/decorationBase.ts +++ /dev/null @@ -1,127 +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 { Range } from '../../../../../../../../../editor/common/core/range.js'; -import { IMarkdownString } from '../../../../../../../../../base/common/htmlContent.js'; -import { BaseToken } from '../../../../codecs/base/baseToken.js'; -import { TrackedRangeStickiness } from '../../../../../../../../../editor/common/model.js'; -import type { TAddAccessor, TChangeAccessor, TDecorationStyles, TRemoveAccessor } from './types.js'; -import { ModelDecorationOptions } from '../../../../../../../../../editor/common/model/textModel.js'; - -/** - * Base class for all editor decorations. - */ -export abstract class DecorationBase< - TPromptToken extends BaseToken, - TCssClassName extends string = string, -> { - /** - * Description of the decoration. - */ - protected abstract get description(): string; - - /** - * Default CSS class name of the decoration. - */ - protected abstract get className(): TCssClassName; - - /** - * Inline CSS class name of the decoration. - */ - protected abstract get inlineClassName(): TCssClassName; - - /** - * Indicates whether the decoration spans the whole line(s). - */ - protected get isWholeLine(): boolean { - return false; - } - - /** - * Hover message of the decoration. - */ - protected get hoverMessage(): IMarkdownString | IMarkdownString[] | null { - return null; - } - - /** - * ID of editor decoration it was registered with. - */ - public readonly id: string; - - constructor( - accessor: TAddAccessor, - protected readonly token: TPromptToken, - ) { - this.id = accessor.addDecoration(this.range, this.decorationOptions); - } - - /** - * Range of the decoration. - */ - public get range(): Range { - return this.token.range; - } - - /** - * Changes the decoration in the editor. - */ - public change( - accessor: TChangeAccessor, - ): this { - accessor.changeDecorationOptions( - this.id, - this.decorationOptions, - ); - - return this; - } - - /** - * Removes associated editor decoration(s). - */ - public remove( - accessor: TRemoveAccessor, - ): this { - accessor.removeDecoration(this.id); - - return this; - } - - /** - * Get editor decoration options for this decorator. - */ - private get decorationOptions(): ModelDecorationOptions { - return ModelDecorationOptions.createDynamic({ - description: this.description, - hoverMessage: this.hoverMessage, - className: this.className, - inlineClassName: this.inlineClassName, - isWholeLine: this.isWholeLine, - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - shouldFillLineOnLineBreak: true, - }); - } -} - -/** - * Type of a generic decoration class. - */ -export type TDecorationClass = { - new( - accessor: TAddAccessor, - token: TPromptToken, - ): DecorationBase; - - /** - * CSS styles for the decoration. - */ - readonly cssStyles: TDecorationStyles; - - /** - * Whether the decoration class handles the provided token. - */ - handles(token: BaseToken): token is TPromptToken; -}; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/utils/reactiveDecorationBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/utils/reactiveDecorationBase.ts deleted file mode 100644 index 42533aea5fe..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/utils/reactiveDecorationBase.ts +++ /dev/null @@ -1,162 +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 { DecorationBase } from './decorationBase.js'; -import { Position } from '../../../../../../../../../editor/common/core/position.js'; -import { BaseToken } from '../../../../codecs/base/baseToken.js'; -import type { IReactiveDecorationClassNames, TAddAccessor, TChangeAccessor, TRemoveAccessor } from './types.js'; - -/** - * Base class for all reactive editor decorations. A reactive decoration - * is a decoration that can change its appearance based on current cursor - * position in the editor, hence can "react" to the user's actions. - */ -export abstract class ReactiveDecorationBase< - TPromptToken extends BaseToken, - TCssClassName extends string = string, -> extends DecorationBase { - /** - * CSS class names of the decoration. - */ - protected abstract get classNames(): IReactiveDecorationClassNames; - - /** - * A list of child decorators that are part of this decoration. - * For instance a Front Matter header decoration can have child - * decorators for each of the header's `---` markers. - */ - protected readonly childDecorators: DecorationBase[]; - - /** - * Whether the decoration has changed since the last {@link change}. - */ - public get changed(): boolean { - // if any of the child decorators changed, this object is also - // considered to be changed - for (const marker of this.childDecorators) { - if ((marker instanceof ReactiveDecorationBase) === false) { - continue; - } - - if (marker.changed === true) { - return true; - } - } - - return this.didChange; - } - - constructor( - accessor: TAddAccessor, - token: TPromptToken, - ) { - super(accessor, token); - - this.childDecorators = []; - } - - /** - * Current position of cursor in the editor. - */ - private cursorPosition?: Position | null; - - /** - * Private field for the {@link changed} property. - */ - private didChange = true; - - /** - * Whether cursor is currently inside the decoration range. - */ - protected get active(): boolean { - return true; - - /** - * Temporarily disable until we have a proper way to get - * the cursor position inside active editor. - */ - /** - * if (!this.cursorPosition) { - * return false; - * } - * - * // when cursor is at the end of a range, the range considered to - * // not contain the position, but we want to include it - * const atEnd = (this.range.endLineNumber === this.cursorPosition.lineNumber) - * && (this.range.endColumn === this.cursorPosition.column); - * - * return atEnd || this.range.containsPosition(this.cursorPosition); - */ - } - - /** - * Set cursor position and update {@link changed} property if needed. - */ - public setCursorPosition( - position: Position | null | undefined, - ): this is { readonly changed: true } { - if (this.cursorPosition === position) { - return false; - } - - if (this.cursorPosition && position) { - if (this.cursorPosition.equals(position)) { - return false; - } - } - - const wasActive = this.active; - this.cursorPosition = position; - this.didChange = (wasActive !== this.active); - - return this.changed; - } - - public override change( - accessor: TChangeAccessor, - ): this { - if (this.didChange === false) { - return this; - } - - super.change(accessor); - this.didChange = false; - - for (const marker of this.childDecorators) { - marker.change(accessor); - } - - return this; - } - - public override remove( - accessor: TRemoveAccessor, - ): this { - super.remove(accessor); - - for (const marker of this.childDecorators) { - marker.remove(accessor); - } - - return this; - } - - protected override get className(): TCssClassName { - return (this.active) - ? this.classNames.Main - : this.classNames.MainInactive; - } - - protected override get inlineClassName(): TCssClassName { - return (this.active) - ? this.classNames.Inline - : this.classNames.InlineInactive; - } -} - -/** - * Type for a decorator with {@link ReactiveDecorationBase.changed changed} property set to `true`. - */ -export type TChangedDecorator = ReactiveDecorationBase & { readonly changed: true }; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/utils/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/utils/types.ts deleted file mode 100644 index 1ca7e270d3f..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/decorations/utils/types.ts +++ /dev/null @@ -1,56 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IModelDecorationsChangeAccessor } from '../../../../../../../../../editor/common/model.js'; - -/** - * CSS class names of a `reactive` decoration. - */ -export interface IReactiveDecorationClassNames { - /** - * Main, default CSS class name of the decoration. - */ - readonly Main: T; - - /** - * CSS class name of the decoration for the `inline`(text) styles. - */ - readonly Inline: T; - - /** - * main CSS class name of the decoration for the `inactive` - * decoration state. - */ - readonly MainInactive: T; - - /** - * CSS class name of the decoration for the `inline`(text) - * styles when decoration is in the `inactive` state. - */ - readonly InlineInactive: T; -} - -/** - * CSS styles for a decoration to be registered with editor. - */ -export type TDecorationStyles = { - readonly [key in TClassNames]: readonly string[]; -}; - -/** - * A model decorations accessor that can be used to `add` a decoration. - */ -export type TAddAccessor = Pick; - -/** - * A model decorations accessor that can be used to `change` a decoration. - */ -export type TChangeAccessor = Pick; - -/** - * A model decorations accessor that can be used to `remove` a decoration. - */ -export type TRemoveAccessor = Pick; - diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/promptDecorationsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/promptDecorationsProvider.ts deleted file mode 100644 index d7a9d9b235f..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/promptDecorationsProvider.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 { IPromptsService } from '../../service/promptsService.js'; -import { ProviderInstanceBase } from '../providerInstanceBase.js'; -import { ITextModel } from '../../../../../../../editor/common/model.js'; -import { FrontMatterDecoration } from './decorations/frontMatterDecoration.js'; -import { toDisposable } from '../../../../../../../base/common/lifecycle.js'; -import { Position } from '../../../../../../../editor/common/core/position.js'; -import { BaseToken } from '../../codecs/base/baseToken.js'; -import { ProviderInstanceManagerBase, TProviderClass } from '../providerInstanceManagerBase.js'; -import { registerThemingParticipant } from '../../../../../../../platform/theme/common/themeService.js'; -import { FrontMatterHeader } from '../../codecs/base/markdownExtensionsCodec/tokens/frontMatterHeader.js'; -import { ReactiveDecorationBase, TChangedDecorator } from './decorations/utils/reactiveDecorationBase.js'; -import { DecorationBase, TDecorationClass } from './decorations/utils/decorationBase.js'; - -/** - * Prompt tokens that are decorated by this provider. - */ -type TDecoratedToken = FrontMatterHeader; - -/** - * List of all supported decorations. - */ -const SUPPORTED_DECORATIONS: readonly TDecorationClass[] = Object.freeze([ - FrontMatterDecoration, -]); - -/** - * Prompt syntax decorations provider for text models. - */ -export class PromptDecorator extends ProviderInstanceBase { - /** - * Currently active decorations. - */ - private readonly decorations: DecorationBase[] = []; - - constructor( - model: ITextModel, - @IPromptsService promptsService: IPromptsService, - ) { - super(model, promptsService); - - this.watchCursorPosition(); - } - - protected override async onPromptSettled( - _error?: Error, - ): Promise { - // by the time the promise above completes, either this object - // or the text model might be already has been disposed - if (this.isDisposed || this.model.isDisposed()) { - return; - } - - this.addDecorations(); - - return; - } - - /** - * Get the current cursor position inside an active editor. - * Note! Currently not implemented because the provider is disabled, and - * we need to do some refactoring to get accurate cursor position. - */ - private get cursorPosition(): Position | null { - if (this.model.isDisposed()) { - return null; - } - - return null; - } - - /** - * Watch editor cursor position and update reactive decorations accordingly. - */ - private watchCursorPosition(): this { - const interval = setInterval(() => { - const { cursorPosition } = this; - - const changedDecorations: TChangedDecorator[] = []; - for (const decoration of this.decorations) { - if ((decoration instanceof ReactiveDecorationBase) === false) { - continue; - } - - if (decoration.setCursorPosition(cursorPosition) === true) { - changedDecorations.push(decoration); - } - } - - if (changedDecorations.length === 0) { - return; - } - - this.changeModelDecorations(changedDecorations); - }, 25); - - this._register(toDisposable(() => { - clearInterval(interval); - })); - - return this; - } - - /** - * Update existing decorations. - */ - private changeModelDecorations( - decorations: readonly TChangedDecorator[], - ): this { - this.model.changeDecorations((accessor) => { - for (const decoration of decorations) { - decoration.change(accessor); - } - }); - - return this; - } - - /** - * Add decorations for all prompt tokens. - */ - private addDecorations(): this { - this.model.changeDecorations((accessor) => { - const { tokens } = this.parser; - - // remove all existing decorations - for (const decoration of this.decorations.splice(0)) { - decoration.remove(accessor); - } - - // then add new decorations based on the current tokens - for (const token of tokens) { - for (const Decoration of SUPPORTED_DECORATIONS) { - if (Decoration.handles(token) === false) { - continue; - } - - this.decorations.push( - new Decoration(accessor, token), - ); - break; - } - } - }); - - return this; - } - - /** - * Remove all existing decorations. - */ - private removeAllDecorations(): this { - if (this.decorations.length === 0) { - return this; - } - - this.model.changeDecorations((accessor) => { - for (const decoration of this.decorations.splice(0)) { - decoration.remove(accessor); - } - }); - - return this; - } - - public override dispose(): void { - if (this.isDisposed) { - return; - } - - this.removeAllDecorations(); - super.dispose(); - } - - /** - * Returns a string representation of this object. - */ - public override toString(): string { - return `text-model-prompt-decorator:${this.model.uri.path}`; - } -} - -/** - * Register CSS styles of the supported decorations. - */ -registerThemingParticipant((_theme, collector) => { - for (const Decoration of SUPPORTED_DECORATIONS) { - for (const [className, styles] of Object.entries(Decoration.cssStyles)) { - collector.addRule(`.monaco-editor ${className} { ${styles.join(' ')} }`); - } - } -}); - -/** - * Provider for prompt syntax decorators on text models. - */ -export class PromptDecorationsProviderInstanceManager extends ProviderInstanceManagerBase { - protected override get InstanceClass(): TProviderClass { - return PromptDecorator; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/types.ts deleted file mode 100644 index fb3ef81b122..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/decorationsProvider/types.ts +++ /dev/null @@ -1,48 +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 { IRange } from '../../../../../../../editor/common/core/range.js'; -import { ModelDecorationOptions } from '../../../../../../../editor/common/model/textModel.js'; - -/** - * Decoration object. - */ -export interface ITextModelDecoration { - /** - * Range of the decoration. - */ - range: IRange; - - /** - * Associated decoration options. - */ - options: ModelDecorationOptions; -} - -/** - * Decoration CSS class names. - */ -export enum DecorationClassNames { - /** - * CSS class name for `default` prompt syntax decoration. - */ - Default = 'prompt-decoration', - - /** - * CSS class name for `file reference` prompt syntax decoration. - */ - 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/languageProviders/promptBodyAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptBodyAutocompletion.ts index d77e76a5ce1..beb0435919f 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptBodyAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptBodyAutocompletion.ts @@ -5,12 +5,10 @@ import { dirname, extUri } from '../../../../../../base/common/resources.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { ALL_PROMPTS_LANGUAGE_SELECTOR, PromptsType } from '../promptTypes.js'; +import { PromptsType } from '../promptTypes.js'; import { Position } from '../../../../../../editor/common/core/position.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList } from '../../../../../../editor/common/languages.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { CharCode } from '../../../../../../base/common/charCode.js'; @@ -24,7 +22,7 @@ import { getPromptFileType } from '../config/promptFileLocations.js'; * - #file: paths to files and folders in the workspace * - # tool names */ -export class PromptBodyAutocompletion extends Disposable implements CompletionItemProvider { +export class PromptBodyAutocompletion implements CompletionItemProvider { /** * Debug display name for this provider. */ @@ -37,12 +35,8 @@ export class PromptBodyAutocompletion extends Disposable implements CompletionIt constructor( @IFileService private readonly fileService: IFileService, - @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, ) { - super(); - - this._register(this.languageService.completionProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, this)); } /** @@ -70,7 +64,7 @@ export class PromptBodyAutocompletion extends Disposable implements CompletionIt } private async collectToolCompletions(model: ITextModel, position: Position, toolRange: Range, suggestions: CompletionItem[]): Promise { - const addSuggestion = (toolName: string, toolRange: Range) => { + for (const toolName of this.languageModelToolsService.getQualifiedToolNames()) { suggestions.push({ label: toolName, kind: CompletionItemKind.Value, @@ -78,14 +72,6 @@ export class PromptBodyAutocompletion extends Disposable implements CompletionIt insertText: toolName, range: toolRange, }); - }; - for (const tool of this.languageModelToolsService.getTools()) { - if (tool.canBeReferencedInPrompt) { - addSuggestion(tool.toolReferenceName ?? tool.displayName, toolRange); - } - } - for (const toolSet of this.languageModelToolsService.toolSets.get()) { - addSuggestion(toolSet.referenceName, toolRange); } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts new file mode 100644 index 00000000000..face31b47e8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { CodeActionContext, CodeActionList, CodeActionProvider, ProviderResult, TextEdit, WorkspaceEdit } from '../../../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { localize } from '../../../../../../nls.js'; +import { ILanguageModelToolsService } from '../../languageModelToolsService.js'; +import { getPromptsTypeForLanguageId } from '../promptTypes.js'; +import { IPromptsService } from '../service/promptsService.js'; +import { IValue } from '../service/newPromptsParser.js'; +import { Selection } from '../../../../../../editor/common/core/selection.js'; + +export class PromptCodeActionProvider implements CodeActionProvider { + /** + * Debug display name for this provider. + */ + public readonly _debugDisplayName: string = 'PromptCodeActionProvider'; + + constructor( + @IPromptsService private readonly promptsService: IPromptsService, + @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService + ) { + } + + provideCodeActions(model: ITextModel, range: Range | Selection, context: CodeActionContext, token: CancellationToken): ProviderResult { + const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); + if (!promptType) { + // if the model is not a prompt, we don't provide any code actions + return undefined; + } + + const parser = this.promptsService.getParsedPromptFile(model); + const toolsAttr = parser.header?.getAttribute('tools'); + if (!toolsAttr || toolsAttr.value.type !== 'array' || !toolsAttr.value.range.containsRange(range)) { + return undefined; + } + for (const item of toolsAttr.value.items) { + if (item.range.containsRange(range)) { + return this.getToolCodeActions(item, model); + } + } + return undefined; + } + + private getToolCodeActions(value: IValue, model: ITextModel): CodeActionList | undefined { + if (value.type !== 'string') { + return undefined; + } + const oldName = value.value; + const deprecatedNames = this.languageModelToolsService.getDeprecatedQualifiedToolNames(); + const newName = deprecatedNames.get(oldName); + if (newName) { + const quote = model.getValueInRange(new Range(value.range.startLineNumber, value.range.startColumn, value.range.endLineNumber, value.range.startColumn + 1)); + const text = (quote === `'` || quote === '"') ? (quote + newName + quote) : newName; + return { + actions: [{ + title: localize('replaceWith', "Replace with '{0}'", newName), + edit: asWorkspaceEdit(model, { range: value.range, text: text }) + }], + dispose() { } + }; + } + return undefined; + } + +} +function asWorkspaceEdit(model: ITextModel, textEdit: TextEdit): WorkspaceEdit { + return { + edits: [{ + versionId: model.getVersionId(), + resource: model.uri, + textEdit + }] + }; +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptDocumentSemanticTokensProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptDocumentSemanticTokensProvider.ts new file mode 100644 index 00000000000..9d4ff46fcea --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptDocumentSemanticTokensProvider.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DocumentSemanticTokensProvider, ProviderResult, SemanticTokens, SemanticTokensLegend } from '../../../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { getPromptsTypeForLanguageId } from '../promptTypes.js'; +import { IPromptsService } from '../service/promptsService.js'; + +export class PromptDocumentSemanticTokensProvider implements DocumentSemanticTokensProvider { + /** + * Debug display name for this provider. + */ + public readonly _debugDisplayName: string = 'PromptDocumentSemanticTokensProvider'; + + constructor( + @IPromptsService private readonly promptsService: IPromptsService, + ) { + } + + provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): ProviderResult { + const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); + if (!promptType) { + // if the model is not a prompt, we don't provide any semantic tokens + return undefined; + } + + const parser = this.promptsService.getParsedPromptFile(model); + if (!parser.body) { + return undefined; + } + + const variableReferences = parser.body.variableReferences; + if (!variableReferences.length) { + return undefined; + } + + // Prepare semantic tokens data following the delta-encoded, 5-number tuple format: + // [deltaLine, deltaStart, length, tokenType, tokenModifiers] + // We expose a single token type 'variable' (index 0) and no modifiers (bitset 0). + const data: number[] = []; + let lastLine = 0; + let lastChar = 0; + + // Ensure stable order (parser already produces them in order, but sort defensively) + const ordered = [...variableReferences].sort((a, b) => a.range.startLineNumber === b.range.startLineNumber + ? a.range.startColumn - b.range.startColumn + : a.range.startLineNumber - b.range.startLineNumber); + + for (const ref of ordered) { + const line = ref.range.startLineNumber - 1; // zero-based + const char = ref.range.startColumn - 2; // zero-based, include the leading # + const length = ref.range.endColumn - ref.range.startColumn + 1; + const deltaLine = line - lastLine; + const deltaChar = deltaLine === 0 ? char - lastChar : char; + data.push(deltaLine, deltaChar, length, 0 /* variable token type index */, 0 /* no modifiers */); + lastLine = line; + lastChar = char; + if (token.isCancellationRequested) { + break; // Return what we have so far if cancelled. + } + } + + return { data: new Uint32Array(data) }; + } + + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['variable'], tokenModifiers: [] }; + } + + releaseDocumentSemanticTokens(resultId: string | undefined): void { + // No caching/result management needed for the simple, stateless implementation. + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts index 92c425f90bb..860f7731bef 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -5,24 +5,20 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { CharCode } from '../../../../../../base/common/charCode.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { Position } from '../../../../../../editor/common/core/position.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { CompletionContext, CompletionItem, CompletionItemInsertTextRule, CompletionItemKind, CompletionItemProvider, CompletionList } from '../../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; import { ILanguageModelToolsService } from '../../languageModelToolsService.js'; import { IChatModeService } from '../../chatModes.js'; -import { InstructionsHeader } from '../parsers/promptHeader/instructionsHeader.js'; -import { PromptToolsMetadata } from '../parsers/promptHeader/metadata/tools.js'; -import { ModeHeader } from '../parsers/promptHeader/modeHeader.js'; -import { PromptHeader } from '../parsers/promptHeader/promptHeader.js'; -import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; +import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; import { Iterable } from '../../../../../../base/common/iterator.js'; +import { PromptHeader } from '../service/newPromptsParser.js'; +import { getValidAttributeNames } from './promptValidator.js'; -export class PromptHeaderAutocompletion extends Disposable implements CompletionItemProvider { +export class PromptHeaderAutocompletion implements CompletionItemProvider { /** * Debug display name for this provider. */ @@ -35,14 +31,10 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion constructor( @IPromptsService private readonly promptsService: IPromptsService, - @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @IChatModeService private readonly chatModeService: IChatModeService, ) { - super(); - - this._register(this.languageService.completionProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, this)); } /** @@ -62,27 +54,14 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion return undefined; } - const parser = this.promptsService.getSyntaxParserFor(model); - await parser.start(token).settled(); - - if (token.isCancellationRequested) { - return undefined; - } - + const parser = this.promptsService.getParsedPromptFile(model); const header = parser.header; if (!header) { return undefined; } - const completed = await header.settled; - if (!completed || token.isCancellationRequested) { - return undefined; - } - - const fullHeaderRange = parser.header.range; - const headerRange = new Range(fullHeaderRange.startLineNumber + 1, 0, fullHeaderRange.endLineNumber - 1, model.getLineMaxColumn(fullHeaderRange.endLineNumber - 1),); - - if (!headerRange.containsPosition(position)) { + const headerRange = parser.header.range; + if (position.lineNumber < headerRange.startLineNumber || position.lineNumber >= headerRange.endLineNumber) { // if the position is not inside the header, we don't provide any completions return undefined; } @@ -103,11 +82,11 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion position: Position, headerRange: Range, colonPosition: Position | undefined, - promptType: string, + promptType: PromptsType, ): Promise { const suggestions: CompletionItem[] = []; - const supportedProperties = this.getSupportedProperties(promptType); + const supportedProperties = new Set(getValidAttributeNames(promptType, false)); this.removeUsedProperties(supportedProperties, model, headerRange, position); const getInsertText = (property: string): string => { @@ -140,27 +119,24 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion private async provideValueCompletions( model: ITextModel, position: Position, - header: PromptHeader | ModeHeader | InstructionsHeader, + header: PromptHeader, colonPosition: Position, - promptType: string, + promptType: PromptsType, ): Promise { const suggestions: CompletionItem[] = []; const lineContent = model.getLineContent(position.lineNumber); const property = lineContent.substring(0, colonPosition.column - 1).trim(); - if (!this.getSupportedProperties(promptType).has(property)) { + if (!getValidAttributeNames(promptType, true).includes(property)) { return undefined; } - if (header instanceof PromptHeader || header instanceof ModeHeader) { - const tools = header.metadataUtility.tools; - if (tools) { - // if the position is inside the tools metadata, we provide tool name completions - const result = this.provideToolCompletions(model, position, tools); - if (result) { - return result; - } + if (promptType === PromptsType.prompt || promptType === PromptsType.mode) { + // if the position is inside the tools metadata, we provide tool name completions + const result = this.provideToolCompletions(model, position, header); + if (result) { + return result; } } @@ -185,17 +161,6 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion return { suggestions }; } - private getSupportedProperties(promptType: string): Set { - switch (promptType) { - case PromptsType.instructions: - return new Set(['applyTo', 'description']); - case PromptsType.prompt: - return new Set(['mode', 'tools', 'description', 'model']); - default: - return new Set(['tools', 'description', 'model']); - } - } - private removeUsedProperties(properties: Set, model: ITextModel, headerRange: Range, position: Position): void { for (let i = headerRange.startLineNumber; i <= headerRange.endLineNumber; i++) { if (i !== position.lineNumber) { @@ -244,14 +209,14 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion return result; } - private provideToolCompletions(model: ITextModel, position: Position, node: PromptToolsMetadata): CompletionList | undefined { - const tools = node.value; - if (!tools || !node.range.containsPosition(position)) { + private provideToolCompletions(model: ITextModel, position: Position, header: PromptHeader): CompletionList | undefined { + const toolsAttr = header.getAttribute('tools'); + if (!toolsAttr || toolsAttr.value.type !== 'array' || !toolsAttr.range.containsPosition(position)) { return undefined; } const getSuggestions = (toolRange: Range) => { const suggestions: CompletionItem[] = []; - const addSuggestion = (toolName: string, toolRange: Range) => { + for (const toolName of this.languageModelToolsService.getQualifiedToolNames()) { let insertText: string; if (!toolRange.isEmpty()) { const firstChar = model.getValueInRange(toolRange).charCodeAt(0); @@ -266,23 +231,14 @@ export class PromptHeaderAutocompletion extends Disposable implements Completion insertText: insertText, range: toolRange, }); - }; - for (const tool of this.languageModelToolsService.getTools()) { - if (tool.canBeReferencedInPrompt) { - addSuggestion(tool.toolReferenceName ?? tool.displayName, toolRange); - } - } - for (const toolSet of this.languageModelToolsService.toolSets.get()) { - addSuggestion(toolSet.referenceName, toolRange); } return { suggestions }; }; - for (const tool of tools) { - const toolRange = node.getToolRange(tool); - if (toolRange?.containsPosition(position)) { + for (const toolNameNode of toolsAttr.value.items) { + if (toolNameNode.range.containsPosition(position)) { // if the position is inside a tool range, we provide tool name completions - return getSuggestions(toolRange); + return getSuggestions(toolNameNode.range); } } const prefix = model.getValueInRange(new Range(position.lineNumber, 1, position.lineNumber, position.column)); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts deleted file mode 100644 index b8bca008829..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderDiagnosticsProvider.ts +++ /dev/null @@ -1,234 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IPromptsService } from '../service/promptsService.js'; -import { ProviderInstanceBase } from './providerInstanceBase.js'; -import { ITextModel } from '../../../../../../editor/common/model.js'; -import { assertNever } from '../../../../../../base/common/assert.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { ProviderInstanceManagerBase, TProviderClass } from './providerInstanceManagerBase.js'; -import { TDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../parsers/promptHeader/diagnostics.js'; -import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../platform/markers/common/markers.js'; -import { PromptHeader } from '../parsers/promptHeader/promptHeader.js'; -import { PromptToolsMetadata } from '../parsers/promptHeader/metadata/tools.js'; -import { PromptModelMetadata } from '../parsers/promptHeader/metadata/model.js'; -import { ModeHeader } from '../parsers/promptHeader/modeHeader.js'; -import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; -import { ILanguageModelToolsService } from '../../languageModelToolsService.js'; -import { localize } from '../../../../../../nls.js'; -import { ChatModeKind } from '../../constants.js'; -import { IChatMode, IChatModeService } from '../../chatModes.js'; -import { PromptModeMetadata } from '../parsers/promptHeader/metadata/mode.js'; -import { Iterable } from '../../../../../../base/common/iterator.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, - @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, - @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, - @IChatModeService private readonly chatModeService: IChatModeService, - ) { - super(model, promptsService); - this._register(languageModelsService.onDidChangeLanguageModels(() => { - this.onPromptSettled(undefined, CancellationToken.None); - })); - this._register(languageModelToolsService.onDidChangeTools(() => { - this.onPromptSettled(undefined, CancellationToken.None); - })); - this._register(chatModeService.onDidChangeChatModes(() => { - this.onPromptSettled(undefined, CancellationToken.None); - })); - } - - /** - * Update diagnostic markers for the current editor. - */ - protected override async onPromptSettled( - _error: Error | undefined, - token: CancellationToken, - ): Promise { - - const { header } = this.parser; - if (header === undefined) { - this.markerService.remove(MARKERS_OWNER_ID, [this.model.uri]); - return; - } - - // header parsing process is separate from the prompt parsing one, hence - // apply markers only after the header is settled and so has diagnostics - const completed = await header.settled; - if (!completed || token.isCancellationRequested) { - return; - } - - const markers: IMarkerData[] = []; - for (const diagnostic of header.diagnostics) { - markers.push(toMarker(diagnostic)); - } - - if (header instanceof PromptHeader) { - const mode = this.validateMode(header.metadataUtility.mode, markers); - this.validateTools(header.metadataUtility.tools, mode?.kind, markers); - this.validateModel(header.metadataUtility.model, mode?.kind, markers); - } else if (header instanceof ModeHeader) { - this.validateTools(header.metadataUtility.tools, ChatModeKind.Agent, markers); - this.validateModel(header.metadataUtility.model, ChatModeKind.Agent, markers); - - } - - if (markers.length === 0) { - this.markerService.remove(MARKERS_OWNER_ID, [this.model.uri]); - return; - } - - this.markerService.changeOne( - MARKERS_OWNER_ID, - this.model.uri, - markers, - ); - return; - } - validateModel(modelNode: PromptModelMetadata | undefined, modeKind: string | ChatModeKind | undefined, markers: IMarkerData[]) { - if (!modelNode || modelNode.value === undefined) { - return; - } - const languageModes = this.languageModelsService.getLanguageModelIds(); - if (languageModes.length === 0) { - // likely the service is not initialized yet - return; - } - const modelMetadata = this.findModelByName(languageModes, modelNode.value); - if (!modelMetadata) { - markers.push({ - message: localize('promptHeaderDiagnosticsProvider.modelNotFound', "Unknown model '{0}'", modelNode.value), - severity: MarkerSeverity.Warning, - ...modelNode.range, - }); - } else if (modeKind === ChatModeKind.Agent && !ILanguageModelChatMetadata.suitableForAgentMode(modelMetadata)) { - markers.push({ - message: localize('promptHeaderDiagnosticsProvider.modelNotSuited', "Model '{0}' is not suited for agent mode", modelNode.value), - severity: MarkerSeverity.Warning, - ...modelNode.range, - }); - } - - } - findModelByName(languageModes: string[], modelName: string): ILanguageModelChatMetadata | undefined { - for (const model of languageModes) { - const metadata = this.languageModelsService.lookupLanguageModel(model); - if (metadata && metadata.isUserSelectable !== false && ILanguageModelChatMetadata.matchesQualifiedName(modelName, metadata)) { - return metadata; - } - } - return undefined; - } - - validateTools(tools: PromptToolsMetadata | undefined, modeKind: string | ChatModeKind | undefined, markers: IMarkerData[]) { - if (!tools || tools.value === undefined || modeKind === ChatModeKind.Ask || modeKind === ChatModeKind.Edit) { - return; - } - const toolNames = new Set(tools.value); - if (toolNames.size === 0) { - return; - } - for (const tool of this.languageModelToolsService.getTools()) { - toolNames.delete(tool.toolReferenceName ?? tool.displayName); - } - for (const toolSet of this.languageModelToolsService.toolSets.get()) { - toolNames.delete(toolSet.referenceName); - } - - for (const toolName of toolNames) { - const range = tools.getToolRange(toolName); - if (range) { - markers.push({ - message: localize('promptHeaderDiagnosticsProvider.toolNotFound', "Unknown tool '{0}'", toolName), - severity: MarkerSeverity.Warning, - ...range, - }); - } - } - } - - validateMode(modeNode: PromptModeMetadata | undefined, markers: IMarkerData[]): IChatMode | undefined { - if (!modeNode || modeNode.value === undefined) { - return; - } - - const modeValue = modeNode.value; - const modes = this.chatModeService.getModes(); - const availableModes = []; - - // Check if mode exists in builtin or custom modes - for (const mode of Iterable.concat(modes.builtin, modes.custom)) { - if (mode.name === modeValue) { - return mode; - } - availableModes.push(mode.name); // collect all available mode names - } - - markers.push({ - message: localize('promptHeaderDiagnosticsProvider.modeNotFound', "Unknown mode '{0}'. Available modes: {1}", modeValue, availableModes.join(', ')), - severity: MarkerSeverity.Warning, - ...modeNode.range, - }); - return undefined; - - } - - /** - * Returns a string representation of this object. - */ - public override toString(): string { - return `prompt-header-diagnostics:${this.model.uri.path}`; - } -} - -/** - * Convert a provided diagnostic object into a marker data object. - */ -function 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(): TProviderClass { - return PromptHeaderDiagnosticsProvider; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts similarity index 67% rename from src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts rename to src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index 9072b2bf21f..571bfdbfb28 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -5,40 +5,30 @@ import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { Position } from '../../../../../../editor/common/core/position.js'; import { Range } from '../../../../../../editor/common/core/range.js'; import { Hover, HoverContext, HoverProvider } from '../../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; import { localize } from '../../../../../../nls.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; import { ILanguageModelToolsService, ToolSet } from '../../languageModelToolsService.js'; import { IChatModeService, isBuiltinChatMode } from '../../chatModes.js'; -import { InstructionsHeader } from '../parsers/promptHeader/instructionsHeader.js'; -import { PromptModelMetadata } from '../parsers/promptHeader/metadata/model.js'; -import { PromptToolsMetadata } from '../parsers/promptHeader/metadata/tools.js'; -import { ModeHeader } from '../parsers/promptHeader/modeHeader.js'; -import { ALL_PROMPTS_LANGUAGE_SELECTOR, getPromptsTypeForLanguageId } from '../promptTypes.js'; +import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; -import { PromptModeMetadata } from '../parsers/promptHeader/metadata/mode.js'; +import { IHeaderAttribute, PromptBody, PromptHeader } from '../service/newPromptsParser.js'; -export class PromptHeaderHoverProvider extends Disposable implements HoverProvider { +export class PromptHoverProvider implements HoverProvider { /** * Debug display name for this provider. */ - public readonly _debugDisplayName: string = 'PromptHeaderHoverProvider'; + public readonly _debugDisplayName: string = 'PromptHoverProvider'; constructor( @IPromptsService private readonly promptsService: IPromptsService, - @ILanguageFeaturesService private readonly languageService: ILanguageFeaturesService, @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, @IChatModeService private readonly chatModeService: IChatModeService, ) { - super(); - - this._register(this.languageService.hoverProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, this)); } private createHover(contents: string, range: Range): Hover { @@ -48,73 +38,72 @@ export class PromptHeaderHoverProvider extends Disposable implements HoverProvid }; } - public async provideHover( - model: ITextModel, - position: Position, - token: CancellationToken, - _context?: HoverContext - ): Promise { + public async provideHover(model: ITextModel, position: Position, token: CancellationToken, _context?: HoverContext): Promise { const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); if (!promptType) { - // if the model is not a prompt, we don't provide any completions + // if the model is not a prompt, we don't provide any hovers return undefined; } - const parser = this.promptsService.getSyntaxParserFor(model); - await parser.start(token).settled(); - - if (token.isCancellationRequested) { - return undefined; + const parser = this.promptsService.getParsedPromptFile(model); + if (parser.header?.range.containsPosition(position)) { + return this.provideHeaderHover(position, promptType, parser.header); } - - const header = parser.header; - if (!header) { - return undefined; + if (parser.body?.range.containsPosition(position)) { + return this.provideBodyHover(position, parser.body); } + return undefined; + } - const completed = await header.settled; - if (!completed || token.isCancellationRequested) { - return undefined; + private async provideBodyHover(position: Position, body: PromptBody): Promise { + for (const ref of body.variableReferences) { + if (ref.range.containsPosition(position)) { + const toolName = ref.name; + return this.getToolHoverByName(toolName, ref.range); + } } + return undefined; + } - if (header instanceof InstructionsHeader) { - const descriptionRange = header.metadataUtility.description?.range; + private async provideHeaderHover(position: Position, promptType: PromptsType, header: PromptHeader): Promise { + if (promptType === PromptsType.instructions) { + const descriptionRange = header.getAttribute('description')?.range; if (descriptionRange?.containsPosition(position)) { return this.createHover(localize('promptHeader.instructions.description', 'The description of the instruction file. It can be used to provide additional context or information about the instructions and is passed to the language model as part of the prompt.'), descriptionRange); } - const applyToRange = header.metadataUtility.applyTo?.range; + const applyToRange = header.getAttribute('applyTo')?.range; if (applyToRange?.containsPosition(position)) { return this.createHover(localize('promptHeader.instructions.applyToRange', 'One or more glob pattern (separated by comma) that describe for which files the instructions apply to. Based on these patterns, the file is automatically included in the prompt, when the context contains a file that matches one or more of these patterns. Use `**` when you want this file to always be added.\nExample: **/*.ts, **/*.js, client/**'), applyToRange); } - } else if (header instanceof ModeHeader) { - const descriptionRange = header.metadataUtility.description?.range; + } else if (promptType === PromptsType.mode) { + const descriptionRange = header.getAttribute('description')?.range; if (descriptionRange?.containsPosition(position)) { return this.createHover(localize('promptHeader.mode.description', 'The description of the mode file. It can be used to provide additional context or information about the mode to the mode author.'), descriptionRange); } - const model = header.metadataUtility.model; + const model = header.getAttribute('model'); if (model?.range.containsPosition(position)) { return this.getModelHover(model, model.range, localize('promptHeader.mode.model', 'The model to use in this mode.')); } - const tools = header.metadataUtility.tools; + const tools = header.getAttribute('tools'); if (tools?.range.containsPosition(position)) { return this.getToolHover(tools, position, localize('promptHeader.mode.tools', 'The tools to use in this mode.')); } } else { - const descriptionRange = header.metadataUtility.description?.range; + const descriptionRange = header.getAttribute('description')?.range; if (descriptionRange?.containsPosition(position)) { return this.createHover(localize('promptHeader.prompt.description', 'The description of the prompt file. It can be used to provide additional context or information about the prompt to the prompt author.'), descriptionRange); } - const model = header.metadataUtility.model; + const model = header.getAttribute('model'); if (model?.range.containsPosition(position)) { return this.getModelHover(model, model.range, localize('promptHeader.prompt.model', 'The model to use in this prompt.')); } - const tools = header.metadataUtility.tools; + const tools = header.getAttribute('tools'); if (tools?.range.containsPosition(position)) { return this.getToolHover(tools, position, localize('promptHeader.prompt.tools', 'The tools to use in this prompt.')); } - const mode = header.metadataUtility.mode; + const mode = header.getAttribute('mode'); if (mode?.range.containsPosition(position)) { return this.getModeHover(mode, position, localize('promptHeader.prompt.mode', 'The mode to use in this prompt.')); } @@ -122,26 +111,29 @@ export class PromptHeaderHoverProvider extends Disposable implements HoverProvid return undefined; } - private getToolHover(node: PromptToolsMetadata, position: Position, baseMessage: string): Hover | undefined { - if (node.value) { - - for (const toolName of node.value) { - const toolRange = node.getToolRange(toolName); - if (toolRange?.containsPosition(position)) { - const tool = this.languageModelToolsService.getToolByName(toolName); - if (tool) { - return this.createHover(tool.modelDescription, toolRange); - } - const toolSet = this.languageModelToolsService.getToolSetByName(toolName); - if (toolSet) { - return this.getToolsetHover(toolSet, toolRange); - } + private getToolHover(node: IHeaderAttribute, position: Position, baseMessage: string): Hover | undefined { + if (node.value.type === 'array') { + for (const toolName of node.value.items) { + if (toolName.type === 'string' && toolName.range.containsPosition(position)) { + return this.getToolHoverByName(toolName.value, toolName.range); } } } return this.createHover(baseMessage, node.range); } + private getToolHoverByName(toolName: string, range: Range): Hover | undefined { + const tool = this.languageModelToolsService.getToolByQualifiedName(toolName); + if (tool !== undefined) { + if (tool instanceof ToolSet) { + return this.getToolsetHover(tool, range); + } else { + return this.createHover(tool.modelDescription, range); + } + } + return undefined; + } + private getToolsetHover(toolSet: ToolSet, range: Range): Hover | undefined { const lines: string[] = []; lines.push(localize('toolSetName', 'ToolSet: {0}\n\n', toolSet.referenceName)); @@ -154,12 +146,11 @@ export class PromptHeaderHoverProvider extends Disposable implements HoverProvid return this.createHover(lines.join('\n'), range); } - private getModelHover(node: PromptModelMetadata, range: Range, baseMessage: string): Hover | undefined { - const modelName = node.value; - if (modelName) { + private getModelHover(node: IHeaderAttribute, range: Range, baseMessage: string): Hover | undefined { + if (node.value.type === 'string') { for (const id of this.languageModelsService.getLanguageModelIds()) { const meta = this.languageModelsService.lookupLanguageModel(id); - if (meta && ILanguageModelChatMetadata.matchesQualifiedName(modelName, meta)) { + if (meta && ILanguageModelChatMetadata.matchesQualifiedName(node.value.value, meta)) { const lines: string[] = []; lines.push(baseMessage + '\n'); lines.push(localize('modelName', '- Name: {0}', meta.name)); @@ -175,13 +166,11 @@ export class PromptHeaderHoverProvider extends Disposable implements HoverProvid return this.createHover(baseMessage, range); } - private getModeHover(mode: PromptModeMetadata, position: Position, baseMessage: string): Hover | undefined { + private getModeHover(mode: IHeaderAttribute, position: Position, baseMessage: string): Hover | undefined { const lines: string[] = []; - - const value = mode.value; - if (value && mode.valueRange?.containsPosition(position)) { - const mode = this.chatModeService.findModeByName(value); + if (value.type === 'string' && value.range.containsPosition(position)) { + const mode = this.chatModeService.findModeByName(value.value); if (mode) { const description = mode.description.get() || (isBuiltinChatMode(mode) ? localize('promptHeader.prompt.mode.builtInDesc', 'Built-in chat mode') : localize('promptHeader.prompt.mode.customDesc', 'Custom chat mode')); lines.push(`\`${mode.name}\`: ${description}`); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptLinkDiagnosticsProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptLinkDiagnosticsProvider.ts deleted file mode 100644 index 3809f67ca8f..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptLinkDiagnosticsProvider.ts +++ /dev/null @@ -1,98 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IPromptsService } from '../service/promptsService.js'; -import { IPromptFileReference } from '../parsers/types.js'; -import { ProviderInstanceBase } from './providerInstanceBase.js'; -import { ITextModel } from '../../../../../../editor/common/model.js'; -import { assertDefined } from '../../../../../../base/common/types.js'; -import { ProviderInstanceManagerBase, TProviderClass } from './providerInstanceManagerBase.js'; -import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../platform/markers/common/markers.js'; -import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { localize } from '../../../../../../nls.js'; - -/** - * Unique ID of the markers provider class. - */ -const MARKERS_OWNER_ID = 'prompt-link-diagnostics-provider'; - -/** - * Prompt links diagnostics provider for a single text model. - */ -class PromptLinkDiagnosticsProvider extends ProviderInstanceBase { - constructor( - model: ITextModel, - @IPromptsService promptsService: IPromptsService, - @IMarkerService private readonly markerService: IMarkerService, - @IFileService private readonly fileService: IFileService - ) { - super(model, promptsService); - } - - /** - * Update diagnostic markers for the current editor. - */ - protected override async onPromptSettled(): Promise { - // clean up all previously added markers - this.markerService.remove(MARKERS_OWNER_ID, [this.model.uri]); - - const markers: IMarkerData[] = []; - - const stats = await this.fileService.resolveAll(this.parser.references.map(ref => ({ resource: ref.uri }))); - for (let i = 0; i < stats.length; i++) { - if (!stats[i].success) { - markers.push(toMarker(this.parser.references[i], localize('fileNotFound', 'File not found.'))); - } - } - - this.markerService.changeOne( - MARKERS_OWNER_ID, - this.model.uri, - markers, - ); - } - - /** - * Returns a string representation of this object. - */ - public override toString(): string { - return `prompt-link-diagnostics:${this.model.uri.path}`; - } -} - -/** - * Convert a prompt link with an issue to a marker data. - * - * @throws - * - if there is no link issue (e.g., `topError` undefined) - * - if there is no link range to highlight (e.g., `linkRange` undefined) - * - if the original error is of `NotPromptFile` type - we don't want to - * show diagnostic markers for non-prompt file links in the prompts - */ -function toMarker(link: IPromptFileReference, message: string): IMarkerData { - const { linkRange } = link; - - assertDefined( - linkRange, - 'Link range must to be defined.', - ); - - - return { - message: message, - severity: MarkerSeverity.Warning, - ...linkRange, - }; -} - -/** - * The class that manages creation and disposal of {@link PromptLinkDiagnosticsProvider} - * classes for each specific editor text model. - */ -export class PromptLinkDiagnosticsInstanceManager extends ProviderInstanceManagerBase { - protected override get InstanceClass(): TProviderClass { - return PromptLinkDiagnosticsProvider; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptLinkProvider.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptLinkProvider.ts index 22b498d8b3a..c62ca12cdc8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptLinkProvider.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptLinkProvider.ts @@ -4,10 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IPromptsService } from '../service/promptsService.js'; -import { assert } from '../../../../../../base/common/assert.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; -import { assertDefined } from '../../../../../../base/common/types.js'; -import { CancellationError } from '../../../../../../base/common/errors.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { ILink, ILinksList, LinkProvider } from '../../../../../../editor/common/languages.js'; @@ -23,48 +20,20 @@ export class PromptLinkProvider implements LinkProvider { /** * Provide list of links for the provided text model. */ - public async provideLinks( - model: ITextModel, - token: CancellationToken, - ): Promise { - assert( - !token.isCancellationRequested, - new CancellationError(), - ); - - const parser = this.promptsService.getSyntaxParserFor(model); - assert( - parser.isDisposed === false, - 'Prompt parser must not be disposed.', - ); - - // start the parser in case it was not started yet, - // and wait for it to settle to a final result - const completed = await parser.start(token).settled(); - if (!completed || token.isCancellationRequested) { - return undefined; + public async provideLinks(model: ITextModel, token: CancellationToken): Promise { + const parser = this.promptsService.getParsedPromptFile(model); + if (!parser.body) { + return; } - const { references } = parser; - - // filter out references that are not valid links - const links: ILink[] = references - .map((reference) => { - const { uri, linkRange } = reference; - - // must always be true because of the filter above - assertDefined( - linkRange, - 'Link range must be defined.', - ); - - return { - range: linkRange, - url: uri, - }; - }); - - return { - links, - }; + const links: ILink[] = []; + for (const ref of parser.body.fileReferences) { + if (!ref.isMarkdownLink) { + const url = parser.body.resolveFilePath(ref.content); + if (url) { + links.push({ range: ref.range, url }); + } + } + } + return { links }; } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts new file mode 100644 index 00000000000..82be81eba32 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -0,0 +1,437 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isEmptyPattern, parse, splitGlobAware } from '../../../../../../base/common/glob.js'; +import { Iterable } from '../../../../../../base/common/iterator.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { localize } from '../../../../../../nls.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../platform/markers/common/markers.js'; +import { IChatMode, IChatModeService } from '../../chatModes.js'; +import { ChatModeKind } from '../../constants.js'; +import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; +import { ILanguageModelToolsService } from '../../languageModelToolsService.js'; +import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; +import { IArrayValue, IHeaderAttribute, ParsedPromptFile } from '../service/newPromptsParser.js'; +import { PromptsConfig } from '../config/config.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { Delayer } from '../../../../../../base/common/async.js'; +import { ResourceMap } from '../../../../../../base/common/map.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { IPromptsService } from '../service/promptsService.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; + +const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; + +export class PromptValidator { + constructor( + @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, + @IChatModeService private readonly chatModeService: IChatModeService, + @IFileService private readonly fileService: IFileService, + @ILabelService private readonly labelService: ILabelService + ) { } + + public async validate(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { + promptAST.header?.errors.forEach(error => report(toMarker(error.message, error.range, MarkerSeverity.Error))); + this.validateHeader(promptAST, promptType, report); + await this.validateBody(promptAST, report); + } + + private async validateBody(promptAST: ParsedPromptFile, report: (markers: IMarkerData) => void): Promise { + const body = promptAST.body; + if (!body) { + return; + } + + // Validate file references + const fileReferenceChecks: Promise[] = []; + for (const ref of body.fileReferences) { + const resolved = body.resolveFilePath(ref.content); + if (!resolved) { + report(toMarker(localize('promptValidator.invalidFileReference', "Invalid file reference '{0}'.", ref.content), ref.range, MarkerSeverity.Warning)); + continue; + } + fileReferenceChecks.push((async () => { + try { + const exists = await this.fileService.exists(resolved); + if (exists) { + return; + } + } catch { + } + const loc = this.labelService.getUriLabel(resolved); + report(toMarker(localize('promptValidator.fileNotFound', "File '{0}' not found at '{1}'.", ref.content, loc), ref.range, MarkerSeverity.Warning)); + })()); + } + + // Validate variable references (tool or toolset names) + if (body.variableReferences.length) { + const available = new Set(this.languageModelToolsService.getQualifiedToolNames()); + const deprecatedNames = this.languageModelToolsService.getDeprecatedQualifiedToolNames(); + for (const variable of body.variableReferences) { + if (!available.has(variable.name)) { + if (deprecatedNames.has(variable.name)) { + const currentName = deprecatedNames.get(variable.name); + report(toMarker(localize('promptValidator.deprecatedVariableReference', "Tool or toolset '{0}' is deprecated, use '{1}' instead.", variable.name, currentName), variable.range, MarkerSeverity.Warning)); + } else { + report(toMarker(localize('promptValidator.unknownVariableReference', "Unknown tool or toolset '{0}'.", variable.name), variable.range, MarkerSeverity.Error)); + } + } + } + } + + await Promise.all(fileReferenceChecks); + } + + private validateHeader(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): void { + const header = promptAST.header; + if (!header) { + return; + } + const validAttributeNames = getValidAttributeNames(promptType, true); + const attributes = header.attributes; + for (const attribute of attributes) { + if (!validAttributeNames.includes(attribute.key)) { + const supportedNames = getValidAttributeNames(promptType, false).join(', '); + switch (promptType) { + case PromptsType.prompt: + report(toMarker(localize('promptValidator.unknownAttribute.prompt', "Attribute '{0}' is not supported in prompt files. Supported: {1}.", attribute.key, supportedNames), attribute.range, MarkerSeverity.Warning)); + break; + case PromptsType.mode: + report(toMarker(localize('promptValidator.unknownAttribute.mode', "Attribute '{0}' is not supported in mode files. Supported: {1}.", attribute.key, supportedNames), attribute.range, MarkerSeverity.Warning)); + break; + case PromptsType.instructions: + report(toMarker(localize('promptValidator.unknownAttribute.instructions', "Attribute '{0}' is not supported in instructions files. Supported: {1}.", attribute.key, supportedNames), attribute.range, MarkerSeverity.Warning)); + break; + } + } + } + this.validateDescription(attributes, report); + switch (promptType) { + case PromptsType.prompt: { + const mode = this.validateMode(attributes, report); + this.validateTools(attributes, mode?.kind ?? ChatModeKind.Agent, report); + this.validateModel(attributes, mode?.kind ?? ChatModeKind.Agent, report); + break; + } + case PromptsType.instructions: + this.validateApplyTo(attributes, report); + this.validateExcludeMode(attributes, report); + break; + + case PromptsType.mode: + this.validateTools(attributes, ChatModeKind.Agent, report); + this.validateModel(attributes, ChatModeKind.Agent, report); + break; + + } + } + + private validateDescription(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): void { + const descriptionAttribute = attributes.find(attr => attr.key === 'description'); + if (!descriptionAttribute) { + return; + } + if (descriptionAttribute.value.type !== 'string') { + report(toMarker(localize('promptValidator.descriptionMustBeString', "The 'description' attribute must be a string."), descriptionAttribute.range, MarkerSeverity.Error)); + return; + } + if (descriptionAttribute.value.value.trim().length === 0) { + report(toMarker(localize('promptValidator.descriptionShouldNotBeEmpty', "The 'description' attribute should not be empty."), descriptionAttribute.value.range, MarkerSeverity.Error)); + return; + } + } + + + private validateModel(attributes: IHeaderAttribute[], modeKind: ChatModeKind, report: (markers: IMarkerData) => void): void { + const attribute = attributes.find(attr => attr.key === 'model'); + if (!attribute) { + return; + } + if (attribute.value.type !== 'string') { + report(toMarker(localize('promptValidator.modelMustBeString', "The 'model' attribute must be a string."), attribute.value.range, MarkerSeverity.Error)); + return; + } + const modelName = attribute.value.value.trim(); + if (modelName.length === 0) { + report(toMarker(localize('promptValidator.modelMustBeNonEmpty', "The 'model' attribute must be a non-empty string."), attribute.value.range, MarkerSeverity.Error)); + return; + } + + const languageModes = this.languageModelsService.getLanguageModelIds(); + if (languageModes.length === 0) { + // likely the service is not initialized yet + return; + } + const modelMetadata = this.findModelByName(languageModes, modelName); + if (!modelMetadata) { + report(toMarker(localize('promptValidator.modelNotFound', "Unknown model '{0}'.", modelName), attribute.value.range, MarkerSeverity.Warning)); + + } else if (modeKind === ChatModeKind.Agent && !ILanguageModelChatMetadata.suitableForAgentMode(modelMetadata)) { + report(toMarker(localize('promptValidator.modelNotSuited', "Model '{0}' is not suited for agent mode.", modelName), attribute.value.range, MarkerSeverity.Warning)); + } + } + + private findModelByName(languageModes: string[], modelName: string): ILanguageModelChatMetadata | undefined { + for (const model of languageModes) { + const metadata = this.languageModelsService.lookupLanguageModel(model); + if (metadata && metadata.isUserSelectable !== false && ILanguageModelChatMetadata.matchesQualifiedName(modelName, metadata)) { + return metadata; + } + } + return undefined; + } + + private validateMode(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): IChatMode | undefined { + const attribute = attributes.find(attr => attr.key === 'mode'); + if (!attribute) { + return undefined; // default mode for prompts is Agent + } + if (attribute.value.type !== 'string') { + report(toMarker(localize('promptValidator.modeMustBeString', "The 'mode' attribute must be a string."), attribute.value.range, MarkerSeverity.Error)); + return undefined; + } + const modeValue = attribute.value.value; + if (modeValue.trim().length === 0) { + report(toMarker(localize('promptValidator.modeMustBeNonEmpty', "The 'mode' attribute must be a non-empty string."), attribute.value.range, MarkerSeverity.Error)); + return undefined; + } + + const modes = this.chatModeService.getModes(); + const availableModes = []; + + // Check if mode exists in builtin or custom modes + for (const mode of Iterable.concat(modes.builtin, modes.custom)) { + if (mode.name === modeValue) { + return mode; + } + availableModes.push(mode.name); // collect all available mode names + } + + const errorMessage = localize('promptValidator.modeNotFound', "Unknown mode '{0}'. Available modes: {1}.", modeValue, availableModes.join(', ')); + report(toMarker(errorMessage, attribute.value.range, MarkerSeverity.Warning)); + return undefined; + } + + private validateTools(attributes: IHeaderAttribute[], modeKind: ChatModeKind, report: (markers: IMarkerData) => void): undefined { + const attribute = attributes.find(attr => attr.key === 'tools'); + if (!attribute) { + return; + } + if (modeKind !== ChatModeKind.Agent) { + report(toMarker(localize('promptValidator.toolsOnlyInAgent', "The 'tools' attribute is only supported in agent mode. Attribute will be ignored."), attribute.range, MarkerSeverity.Warning)); + } + + switch (attribute.value.type) { + case 'array': + this.validateToolsArray(attribute.value, report); + break; + case 'object': + //this.validateToolsObject(attribute.value, report); + break; + default: + report(toMarker(localize('promptValidator.toolsMustBeArrayOrMap', "The 'tools' attribute must be an array."), attribute.value.range, MarkerSeverity.Error)); + } + } + + private validateToolsArray(valueItem: IArrayValue, report: (markers: IMarkerData) => void) { + if (valueItem.items.length > 0) { + const available = new Set(this.languageModelToolsService.getQualifiedToolNames()); + const deprecatedNames = this.languageModelToolsService.getDeprecatedQualifiedToolNames(); + for (const item of valueItem.items) { + if (item.type !== 'string') { + report(toMarker(localize('promptValidator.eachToolMustBeString', "Each tool name in the 'tools' attribute must be a string."), item.range, MarkerSeverity.Error)); + } else if (item.value && !available.has(item.value)) { + if (deprecatedNames.has(item.value)) { + const currentName = deprecatedNames.get(item.value); + report(toMarker(localize('promptValidator.toolDeprecated', "Tool or toolset '{0}' is deprecated, use '{1}' instead.", item.value, currentName), item.range, MarkerSeverity.Warning)); + } else { + report(toMarker(localize('promptValidator.toolNotFound', "Unknown tool '{0}'.", item.value), item.range, MarkerSeverity.Error)); + } + } + } + } + } + + private validateApplyTo(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { + const attribute = attributes.find(attr => attr.key === 'applyTo'); + if (!attribute) { + return; + } + if (attribute.value.type !== 'string') { + report(toMarker(localize('promptValidator.applyToMustBeString', "The 'applyTo' attribute must be a string."), attribute.value.range, MarkerSeverity.Error)); + return; + } + const pattern = attribute.value.value; + try { + const patterns = splitGlobAware(pattern, ','); + if (patterns.length === 0) { + report(toMarker(localize('promptValidator.applyToMustBeValidGlob', "The 'applyTo' attribute must be a valid glob pattern."), attribute.value.range, MarkerSeverity.Error)); + return; + } + for (const pattern of patterns) { + const globPattern = parse(pattern); + if (isEmptyPattern(globPattern)) { + report(toMarker(localize('promptValidator.applyToMustBeValidGlob', "The 'applyTo' attribute must be a valid glob pattern."), attribute.value.range, MarkerSeverity.Error)); + return; + } + } + } catch (_error) { + report(toMarker(localize('promptValidator.applyToMustBeValidGlob', "The 'applyTo' attribute must be a valid glob pattern."), attribute.value.range, MarkerSeverity.Error)); + } + } + + private validateExcludeMode(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { + const attribute = attributes.find(attr => attr.key === 'excludeMode'); + if (!attribute) { + return; + } + if (attribute.value.type !== 'array') { + report(toMarker(localize('promptValidator.excludeModeMustBeArray', "The 'excludeMode' attribute must be an array."), attribute.value.range, MarkerSeverity.Error)); + return; + } + } +} + +const validAttributeNames = { + [PromptsType.prompt]: ['description', 'model', 'tools', 'mode'], + [PromptsType.instructions]: ['description', 'applyTo', 'excludeMode'], + [PromptsType.mode]: ['description', 'model', 'tools', 'advancedOptions'] +}; +const validAttributeNamesNoExperimental = { + [PromptsType.prompt]: validAttributeNames[PromptsType.prompt].filter(name => !isExperimentalAttribute(name)), + [PromptsType.instructions]: validAttributeNames[PromptsType.instructions].filter(name => !isExperimentalAttribute(name)), + [PromptsType.mode]: validAttributeNames[PromptsType.mode].filter(name => !isExperimentalAttribute(name)) +}; + +export function getValidAttributeNames(promptType: PromptsType, includeExperimental: boolean): string[] { + return includeExperimental ? validAttributeNames[promptType] : validAttributeNamesNoExperimental[promptType]; +} + +export function isExperimentalAttribute(attributeName: string): boolean { + return attributeName === 'advancedOptions' || attributeName === 'excludeMode'; +} + +function toMarker(message: string, range: Range, severity = MarkerSeverity.Error): IMarkerData { + return { severity, message, ...range }; +} + +export class PromptValidatorContribution extends Disposable { + + private readonly validator: PromptValidator; + private readonly localDisposables = this._register(new DisposableStore()); + + constructor( + @IModelService private modelService: IModelService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService private configService: IConfigurationService, + @IMarkerService private readonly markerService: IMarkerService, + @IPromptsService private readonly promptsService: IPromptsService, + @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, + @IChatModeService private readonly chatModeService: IChatModeService, + ) { + super(); + this.validator = instantiationService.createInstance(PromptValidator); + + this.updateRegistration(); + this._register(this.configService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(PromptsConfig.KEY)) { + this.updateRegistration(); + } + })); + } + + updateRegistration(): void { + this.localDisposables.clear(); + if (!PromptsConfig.enabled(this.configService)) { + return; + } + const trackers = new ResourceMap(); + this.localDisposables.add(toDisposable(() => { + trackers.forEach(tracker => tracker.dispose()); + })); + + const validateAllDelayer = this._register(new Delayer(200)); + const validateAll = (): void => { + validateAllDelayer.trigger(async () => { + this.modelService.getModels().forEach(model => { + const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); + if (promptType) { + trackers.set(model.uri, new ModelTracker(model, promptType, this.validator, this.promptsService, this.markerService)); + } + }); + }); + }; + this.localDisposables.add(this.modelService.onModelAdded((model) => { + const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); + if (promptType) { + trackers.set(model.uri, new ModelTracker(model, promptType, this.validator, this.promptsService, this.markerService)); + } + })); + this.localDisposables.add(this.modelService.onModelRemoved((model) => { + const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); + if (promptType) { + const tracker = trackers.get(model.uri); + if (tracker) { + tracker.dispose(); + trackers.delete(model.uri); + } + } + })); + this.localDisposables.add(this.modelService.onModelLanguageChanged((event) => { + const { model } = event; + const tracker = trackers.get(model.uri); + if (tracker) { + tracker.dispose(); + trackers.delete(model.uri); + } + const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); + if (promptType) { + trackers.set(model.uri, new ModelTracker(model, promptType, this.validator, this.promptsService, this.markerService)); + } + })); + this.localDisposables.add(this.languageModelToolsService.onDidChangeTools(() => validateAll())); + this.localDisposables.add(this.chatModeService.onDidChangeChatModes(() => validateAll())); + this.localDisposables.add(this.languageModelsService.onDidChangeLanguageModels(() => validateAll())); + validateAll(); + } +} + +class ModelTracker extends Disposable { + + private readonly delayer: Delayer; + + constructor( + private readonly textModel: ITextModel, + private readonly promptType: PromptsType, + private readonly validator: PromptValidator, + @IPromptsService private readonly promptsService: IPromptsService, + @IMarkerService private readonly markerService: IMarkerService, + ) { + super(); + this.delayer = this._register(new Delayer(200)); + this._register(textModel.onDidChangeContent(() => this.validate())); + this.validate(); + } + + private validate(): void { + this.delayer.trigger(async () => { + const markers: IMarkerData[] = []; + const ast = this.promptsService.getParsedPromptFile(this.textModel); + await this.validator.validate(ast, this.promptType, m => markers.push(m)); + this.markerService.changeOne(MARKERS_OWNER_ID, this.textModel.uri, markers); + }); + } + + public override dispose() { + this.markerService.remove(MARKERS_OWNER_ID, [this.textModel.uri]); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/providerInstanceBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/providerInstanceBase.ts deleted file mode 100644 index da51052ab58..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/providerInstanceBase.ts +++ /dev/null @@ -1,54 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IPromptsService, TSharedPrompt } from '../service/promptsService.js'; -import { ITextModel } from '../../../../../../editor/common/model.js'; -import { ObservableDisposable } from '../utils/observableDisposable.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; - -/** - * Abstract base class for all reusable prompt file providers. - */ -export abstract class ProviderInstanceBase extends ObservableDisposable { - /** - * Function that is called when the prompt parser is settled. - */ - protected abstract onPromptSettled(error: Error | undefined, token: CancellationToken): Promise; - - /** - * Returns a string representation of this object. - */ - public abstract override toString(): string; - - /** - * The prompt parser instance. - */ - protected readonly parser: TSharedPrompt; - - constructor( - protected readonly model: ITextModel, - @IPromptsService promptsService: IPromptsService, - ) { - super(); - - this.parser = promptsService.getSyntaxParserFor(model); - - this._register( - this.parser.onDispose(this.dispose.bind(this)), - ); - - let cancellationSource = new CancellationTokenSource(); - this._register( - this.parser.onSettled((error) => { - cancellationSource.dispose(true); - cancellationSource = new CancellationTokenSource(); - - this.onPromptSettled(error, cancellationSource.token); - }), - ); - - this.parser.start(); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/providerInstanceManagerBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/providerInstanceManagerBase.ts deleted file mode 100644 index 36ff2339b49..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/providerInstanceManagerBase.ts +++ /dev/null @@ -1,176 +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 { ProviderInstanceBase } from './providerInstanceBase.js'; -import { assert } from '../../../../../../base/common/assert.js'; -import { ITextModel } from '../../../../../../editor/common/model.js'; -import { assertDefined } from '../../../../../../base/common/types.js'; -import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { ObjectCache } from '../utils/objectCache.js'; -import { INSTRUCTIONS_LANGUAGE_ID, MODE_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../promptTypes.js'; -import { IModelService } from '../../../../../../editor/common/services/model.js'; -import { PromptsConfig } from '../config/config.js'; -import { IEditorService } from '../../../../../services/editor/common/editorService.js'; -import { IDiffEditor, IEditor, IEditorModel } from '../../../../../../editor/common/editorCommon.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; - -/** - * Type for a text editor that is used for reusable prompt files. - */ -export interface IPromptFileEditor extends IEditor { - readonly getModel: () => ITextModel; -} - -/** - * Type for a class that can create a new provider instance. - */ -export type TProviderClass = new (editor: ITextModel, ...args: any[]) => TInstance; - -/** - * A generic base class that manages creation and disposal of {@link TInstance} - * objects for each specific editor object that is used for reusable prompt files. - */ -export abstract class ProviderInstanceManagerBase extends Disposable { - /** - * Currently available {@link TInstance} instances. - */ - private readonly instances: ObjectCache; - - /** - * Class object of the managed {@link TInstance}. - */ - protected abstract get InstanceClass(): TProviderClass; - - constructor( - @IModelService modelService: IModelService, - @IEditorService editorService: IEditorService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configService: IConfigurationService, - ) { - super(); - - // cache of managed instances - this.instances = this._register( - new ObjectCache((model: ITextModel) => { - assert( - model.isDisposed() === false, - 'Text model must not be disposed.', - ); - - // sanity check - the new TS/JS discrepancies regarding fields initialization - // logic mean that this can be `undefined` during runtime while defined in TS - assertDefined( - this.InstanceClass, - 'Instance class field must be defined.', - ); - - const instance: TInstance = instantiationService.createInstance( - this.InstanceClass, - model, - ); - - // this is a sanity check and the contract of the object cache, - // we must return a non-disposed object from this factory function - instance.assertNotDisposed( - 'Created instance must not be disposed.', - ); - - return instance; - }), - ); - - // if the feature is disabled, do not create any providers - if (PromptsConfig.enabled(configService) === false) { - return; - } - - // subscribe to changes of the active editor - this._register(editorService.onDidActiveEditorChange(() => { - const { activeTextEditorControl } = editorService; - if (activeTextEditorControl === undefined) { - return; - } - - this.handleNewEditor(activeTextEditorControl); - })); - - // handle existing visible text editors - editorService - .visibleTextEditorControls - .forEach(this.handleNewEditor.bind(this)); - - // subscribe to "language change" events for all models - this._register( - modelService.onModelLanguageChanged((event) => { - const { model, oldLanguageId } = event; - - // if language is set to `prompt` or `instructions` language, handle that model - if (isPromptFileModel(model)) { - this.instances.get(model); - return; - } - - // if the language is changed away from `prompt` or `instructions`, - // remove and dispose provider for this model - if (isPromptFile(oldLanguageId)) { - this.instances.remove(model, true); - return; - } - }), - ); - } - - /** - * Initialize a new {@link TInstance} for the given editor. - */ - private handleNewEditor(editor: IEditor | IDiffEditor): this { - const model = editor.getModel(); - if (model === null) { - return this; - } - - if (isPromptFileModel(model) === false) { - return this; - } - - // note! calling `get` also creates a provider if it does not exist; - // and the provider is auto-removed when the editor is disposed - this.instances.get(model); - - return this; - } -} - -/** - * Check if provided language ID is one of the prompt file languages. - */ -function isPromptFile(languageId: string): boolean { - return [ - PROMPT_LANGUAGE_ID, - INSTRUCTIONS_LANGUAGE_ID, - MODE_LANGUAGE_ID, - ].includes(languageId); -} - -/** - * Check if a provided model is used for prompt files. - */ -function isPromptFileModel(model: IEditorModel): model is ITextModel { - // we support only `text editors` for now so filter out `diff` ones - if ('modified' in model || 'model' in model) { - return false; - } - - if (model.isDisposed()) { - return false; - } - - if (isPromptFile(model.getLanguageId()) === false) { - return false; - } - - return true; -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts deleted file mode 100644 index 9fe72885d03..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/basePromptParser.ts +++ /dev/null @@ -1,728 +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 { TopError } from './topError.js'; -import { ChatModeKind } from '../../constants.js'; -import { TMetadata } from './promptHeader/headerBase.js'; -import { ModeHeader } from './promptHeader/modeHeader.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { PromptToken } from '../codecs/tokens/promptToken.js'; -import * as path from '../../../../../../base/common/path.js'; -import { ChatPromptCodec } from '../codecs/chatPromptCodec.js'; -import { FileReference } from '../codecs/tokens/fileReference.js'; -import { ChatPromptDecoder } from '../codecs/chatPromptDecoder.js'; -import { assertDefined } from '../../../../../../base/common/types.js'; -import { Emitter } from '../../../../../../base/common/event.js'; -import { DeferredPromise } from '../../../../../../base/common/async.js'; -import { InstructionsHeader } from './promptHeader/instructionsHeader.js'; -import { ILogService } from '../../../../../../platform/log/common/log.js'; -import { PromptVariable, PromptVariableWithData } from '../codecs/tokens/promptVariable.js'; -import type { IPromptContentsProvider } from '../contentProviders/types.js'; -import type { TPromptReference, ITopError, TVariableReference } from './types.js'; -import { type IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { assert, assertNever } from '../../../../../../base/common/assert.js'; -import { basename, dirname, joinPath } from '../../../../../../base/common/resources.js'; -import { BaseToken } from '../codecs/base/baseToken.js'; -import { VSBufferReadableStream } from '../../../../../../base/common/buffer.js'; -import { type IRange, Range } from '../../../../../../editor/common/core/range.js'; -import { PromptHeader, type TPromptMetadata } from './promptHeader/promptHeader.js'; -import { ObservableDisposable } from '../utils/observableDisposable.js'; -import { INSTRUCTIONS_LANGUAGE_ID, MODE_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../promptTypes.js'; -import { LinesDecoder } from '../codecs/base/linesCodec/linesDecoder.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { MarkdownLink } from '../codecs/base/markdownCodec/tokens/markdownLink.js'; -import { MarkdownToken } from '../codecs/base/markdownCodec/tokens/markdownToken.js'; -import { FrontMatterHeader } from '../codecs/base/markdownExtensionsCodec/tokens/frontMatterHeader.js'; -import { OpenFailed, NotPromptFile, RecursiveReference, FolderReference, ResolveError } from '../../promptFileReferenceErrors.js'; -import { type IPromptContentsProviderOptions } from '../contentProviders/promptContentsProviderBase.js'; -import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; -import { Schemas } from '../../../../../../base/common/network.js'; - -/** - * Options of the {@link BasePromptParser} class. - */ -export interface IBasePromptParserOptions { -} - -export type IPromptParserOptions = IBasePromptParserOptions & IPromptContentsProviderOptions; - - -/** - * Error conditions that may happen during the file reference resolution. - */ -export type TErrorCondition = OpenFailed | RecursiveReference | FolderReference | NotPromptFile; - -/** - * Base prompt parser class that provides a common interface for all - * prompt parsers that are responsible for parsing chat prompt syntax. - */ -export class BasePromptParser extends ObservableDisposable { - /** - * Options passed to the constructor. - */ - protected readonly options: IBasePromptParserOptions; - - /** - * List of all tokens that were parsed from the prompt contents so far. - */ - public get tokens(): readonly BaseToken[] { - return [...this.receivedTokens]; - } - /** - * Private field behind the readonly {@link tokens} property. - */ - private receivedTokens: BaseToken[] = []; - - /** - * List of file references in the prompt file. - */ - private readonly _references: TPromptReference[] = []; - - /** - * List of variable references in the prompt file. - */ - private readonly _variableReferences: TVariableReference[] = []; - - /** - * Reference to the prompt header object that holds metadata associated - * with the prompt. - */ - private promptHeader?: PromptHeader | InstructionsHeader | ModeHeader | undefined; - - /** - * Reference to the prompt header object that holds metadata associated - * with the prompt. - */ - public get header(): PromptHeader | InstructionsHeader | ModeHeader | undefined { - return this.promptHeader; - } - - /** - * Get contents of the prompt body. - */ - public async getBody(): Promise { - const startLineNumber = (this.header !== undefined) - ? this.header.range.endLineNumber + 1 - : 1; - - const decoder = new LinesDecoder( - await this.promptContentsProvider.contents, - ); - - const tokens = (await decoder.consumeAll()) - .filter(({ range }) => { - return (range.startLineNumber >= startLineNumber); - }); - - return BaseToken.render(tokens); - } - - /** - * Get the full contents of the prompt, including the header - */ - public async getFullContent(): Promise { - const decoder = new LinesDecoder( - await this.promptContentsProvider.contents, - ); - const tokens = await decoder.consumeAll(); - return BaseToken.render(tokens); - } - - /** - * The event is fired when lines or their content change. - */ - private readonly _onUpdate = this._register(new Emitter()); - /** - * Subscribe to the event that is fired the parser state or contents - * changes, including changes in the possible prompt child references. - */ - public readonly onUpdate = this._onUpdate.event; - - /** - * Event that is fired when the current prompt parser is settled. - */ - private readonly _onSettled = this._register(new Emitter()); - - /** - * Event that is fired when the current prompt parser is settled. - */ - public onSettled( - callback: (error?: Error) => void, - ): IDisposable { - const disposable = this._onSettled.event(callback); - const streamEnded = (this.stream?.ended && (this.stream.isDisposed === false)); - - // if already in the error state or stream has already ended, - // invoke the callback immediately but asynchronously - if (streamEnded || this.errorCondition) { - setTimeout(callback.bind(undefined, this.errorCondition)); - - return disposable; - } - - return disposable; - } - - /** - * If failed to parse prompt contents, this property has - * an error object that describes the failure reason. - */ - private _errorCondition?: ResolveError; - - /** - * If file reference resolution fails, this attribute will be set - * to an error instance that describes the error condition. - */ - public get errorCondition(): ResolveError | undefined { - return this._errorCondition; - } - - /** - * Whether file references resolution failed. - * Set to `undefined` if the `resolve` method hasn't been ever called yet. - */ - public get resolveFailed(): boolean | undefined { - if (!this.firstParseResult.gotFirstResult) { - return undefined; - } - - return !!this._errorCondition; - } - - /** - * The promise is resolved when at least one parse result (a stream or - * an error) has been received from the prompt contents provider. - */ - private readonly firstParseResult = new FirstParseResult(); - - /** - * Returned promise is resolved when the parser process is settled. - * The settled state means that the prompt parser stream exists and - * has ended, or an error condition has been set in case of failure. - * - * Furthermore, this function can be called multiple times and will - * block until the latest prompt contents parsing logic is settled - * (e.g., for every `onContentChanged` event of the prompt source). - */ - public async settled(): Promise { - assert( - this.started, - 'Cannot wait on the parser that did not start yet.', - ); - - await this.firstParseResult.promise; - - if (this.errorCondition) { - return false; - } - - // by the time when the `firstParseResult` promise is resolved, - // this object may have been already disposed, hence noop - if (this.isDisposed) { - return false; - } - - assertDefined( - this.stream, - 'No stream reference found.', - ); - - const completed = await this.stream.settled; - - // if prompt header exists, also wait for it to be settled - if (this.promptHeader) { - const headerCompleted = await this.promptHeader.settled; - if (!headerCompleted) { - return false; - } - } - - return completed; - } - - constructor( - private readonly promptContentsProvider: TContentsProvider, - options: IBasePromptParserOptions, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IWorkbenchEnvironmentService private readonly envService: IWorkbenchEnvironmentService, - @ILogService protected readonly logService: ILogService, - ) { - super(); - - this.options = options; - - this._register( - this.promptContentsProvider.onContentChanged((streamOrError) => { - // process the received message - this.onContentsChanged(streamOrError); - - // indicate that we've received at least one `onContentChanged` event - this.firstParseResult.end(); - }), - ); - - // dispose self when contents provider is disposed - this._register( - this.promptContentsProvider.onDispose(this.dispose.bind(this)), - ); - } - - /** - * The latest received stream of prompt tokens, if any. - */ - private stream: ChatPromptDecoder | undefined; - - /** - * Handler the event event that is triggered when prompt contents change. - * - * @param streamOrError Either a binary stream of file contents, or an error object - * that was generated during the reference resolve attempt. - * @param seenReferences List of parent references that we've have already seen - * during the process of traversing the references tree. It's - * used to prevent the tree navigation to fall into an infinite - * references recursion. - */ - private onContentsChanged( - streamOrError: VSBufferReadableStream | ResolveError - ): void { - // dispose and cleanup the previously received stream - // object or an error condition, if any received yet - this.stream?.dispose(); - delete this.stream; - delete this._errorCondition; - this.receivedTokens = []; - - // cleanup current prompt header object - this.promptHeader?.dispose(); - delete this.promptHeader; - - // dispose all currently existing references - this.disposeReferences(); - - // if an error received, set up the error condition and stop - if (streamOrError instanceof ResolveError) { - this._errorCondition = streamOrError; - this._onUpdate.fire(); - - // when error received fire the 'onSettled' event immediately - this._onSettled.fire(streamOrError); - - return; - } - - // decode the byte stream to a stream of prompt tokens - this.stream = ChatPromptCodec.decode(streamOrError); - - /** - * !NOTE! The order of event subscriptions below is critical here because - * the `data` event is also starts the stream, hence changing - * the order of event subscriptions can lead to race conditions. - * See {@link ReadableStreamEvents} for more info. - */ - - // on error or stream end, dispose the stream and fire the update event - this.stream.on('error', this.onStreamEnd.bind(this, this.stream)); - this.stream.on('end', this.onStreamEnd.bind(this, this.stream)); - - // when some tokens received, process and store the references - this.stream.on('data', (token) => { - // store all markdown and prompt token references - if ((token instanceof MarkdownToken) || (token instanceof PromptToken)) { - this.receivedTokens.push(token); - } - - // if a prompt header token received, create a new prompt header instance - if (token instanceof FrontMatterHeader) { - return this.createHeader(token); - } - - // try to convert a prompt variable with data token into a file reference - if (token instanceof PromptVariableWithData) { - try { - if (token.name === 'file') { - this.handleLinkToken(FileReference.from(token)); - } - } catch (error) { - // the `FileReference.from` call might throw if the `PromptVariableWithData` token - // can not be converted into a valid `#file` reference, hence we ignore the error - } - } else if (token instanceof PromptVariable) { - this.handleVariableToken(token); - } - - // note! the `isURL` is a simple check and needs to be improved to truly - // handle only file references, ignoring broken URLs or references - if (token instanceof MarkdownLink && !token.isURL) { - this.handleLinkToken(token); - } - }); - - // calling `start` on a disposed stream throws, so we warn and return instead - if (this.stream.isDisposed) { - this.logService.warn( - `[prompt parser][${basename(this.uri)}] cannot start stream that has been already disposed, aborting`, - ); - - return; - } - - // start receiving data on the stream - this.stream.start(); - } - - /** - * Create header object base on the target prompt file language ID. - * The language ID is important here, because it defines what type - * of metadata is valid for a prompt file and what type of related - * diagnostics we would show to the user. - */ - private createHeader(headerToken: FrontMatterHeader): void { - const { languageId } = this.promptContentsProvider; - - if (languageId === PROMPT_LANGUAGE_ID) { - this.promptHeader = new PromptHeader(headerToken, languageId); - } - - if (languageId === INSTRUCTIONS_LANGUAGE_ID) { - this.promptHeader = new InstructionsHeader(headerToken, languageId); - } - - if (languageId === MODE_LANGUAGE_ID) { - this.promptHeader = new ModeHeader(headerToken, languageId); - } - - this.promptHeader?.start(); - } - - /** - * Handle a new reference token inside prompt contents. - */ - private handleLinkToken(token: FileReference | MarkdownLink): this { - - let referenceUri: URI; - if (path.isAbsolute(token.path)) { - referenceUri = URI.file(token.path); - if (this.envService.remoteAuthority) { - referenceUri = referenceUri.with({ - scheme: Schemas.vscodeRemote, - authority: this.envService.remoteAuthority, - }); - } - } else { - referenceUri = joinPath(dirname(this.uri), token.path); - } - this._references.push(new PromptReference(referenceUri, token)); - - this._onUpdate.fire(); - - return this; - } - - private handleVariableToken(token: PromptVariable): this { - - this._variableReferences.push({ name: token.name, range: token.range }); - - this._onUpdate.fire(); - - return this; - } - - /** - * Handle the `stream` end event. - * - * @param stream The stream that has ended. - * @param error Optional error object if stream ended with an error. - */ - private onStreamEnd( - stream: ChatPromptDecoder, - error?: Error, - ): this { - // decoders can fire the 'end' event also when they are get disposed, - // but because we dispose them when a new stream is received, we can - // safely ignore the event in this case - if (stream.isDisposed === true) { - return this; - } - - if (error) { - this.logService.warn( - `[prompt parser][${basename(this.uri)}] received an error on the chat prompt decoder stream: ${error}`, - ); - } - - this._onUpdate.fire(); - this._onSettled.fire(error); - - return this; - } - - - private disposeReferences(): void { - - - this._references.length = 0; - this._variableReferences.length = 0; - } - - /** - * Private attribute to track if the {@link start} - * method has been already called at least once. - */ - private started: boolean = false; - - /** - * Start the prompt parser. - */ - public start(token?: CancellationToken): this { - // if already started, nothing to do - if (this.started) { - return this; - } - this.started = true; - - - // if already in the error state that could be set - // in the constructor, then nothing to do - if (this.errorCondition) { - return this; - } - - this.promptContentsProvider.start(token); - return this; - } - - /** - * Associated URI of the prompt. - */ - public get uri(): URI { - return this.promptContentsProvider.uri; - } - - /** - * Get a list of immediate child references of the prompt. - */ - public get references(): readonly TPromptReference[] { - return [...this._references]; - } - - /** - * Get a list of variable references of the prompt. - */ - public get variableReferences(): readonly TVariableReference[] { - return [...this._variableReferences]; - } - - /** - * Valid metadata records defined in the prompt header. - */ - public get metadata(): TMetadata | null { - const { promptType } = this.promptContentsProvider; - if (promptType === 'non-prompt') { - return null; - } - - if (this.header === undefined) { - return { promptType }; - } - - if (this.header instanceof InstructionsHeader || this.header instanceof ModeHeader) { - return { promptType, ...this.header.metadata }; - } - - const { tools, mode, description, model } = this.header.metadata; - - const result: Partial = {}; - - if (description !== undefined) { - result.description = description; - } - - if (tools !== undefined && mode !== ChatModeKind.Ask && mode !== ChatModeKind.Edit) { - result.tools = tools; - // Preserve custom mode if specified, otherwise default to Agent - result.mode = mode || ChatModeKind.Agent; - } else if (mode !== undefined) { - result.mode = mode; - } - - if (model !== undefined) { - result.model = model; - } - - return { promptType, ...result }; - } - - /** - * The top most error of the current reference or any of its - * possible child reference errors. - */ - public get topError(): ITopError | undefined { - if (this.errorCondition) { - return new TopError({ - errorSubject: 'root', - errorsCount: 1, - originalError: this.errorCondition, - }); - } - - return undefined; - } - - /** - * Returns a string representation of this object. - */ - public override toString(): string { - return `prompt:${this.uri.path}`; - } - - /** - * @inheritdoc - */ - public override dispose(): void { - if (this.isDisposed) { - return; - } - - this.disposeReferences(); - - this.stream?.dispose(); - delete this.stream; - - this.promptHeader?.dispose(); - delete this.promptHeader; - - super.dispose(); - } -} - -/** - * Prompt reference object represents any reference inside prompt text - * contents. For instance the file variable(`#file:/path/to/file.md`) or - * a markdown link(`[#file:file.md](/path/to/file.md)`). - */ -export class PromptReference implements TPromptReference { - - - constructor( - public readonly uri: URI, - public readonly token: FileReference | MarkdownLink, - ) { - } - - /** - * Get the range of the `link` part of the reference. - */ - public get linkRange(): IRange | undefined { - // `#file:` references - if (this.token instanceof FileReference) { - return this.token.dataRange; - } - - // `markdown link` references - if (this.token instanceof MarkdownLink) { - return this.token.linkRange; - } - - return undefined; - } - - /** - * Type of the reference, - either a prompt `#file` variable, - * or a `markdown link` reference (`[caption](/path/to/file.md)`). - */ - public get type(): 'file' { - if (this.token instanceof FileReference) { - return 'file'; - } - - if (this.token instanceof MarkdownLink) { - return 'file'; - } - - assertNever( - this.token, - `Unknown token type '${this.token}'.`, - ); - } - - /** - * Subtype of the reference, - either a prompt `#file` variable, - * or a `markdown link` reference (`[caption](/path/to/file.md)`). - */ - public get subtype(): 'prompt' | 'markdown' { - if (this.token instanceof FileReference) { - return 'prompt'; - } - - if (this.token instanceof MarkdownLink) { - return 'markdown'; - } - - assertNever( - this.token, - `Unknown token type '${this.token}'.`, - ); - } - - public get range(): Range { - return this.token.range; - } - - public get path(): string { - return this.token.path; - } - - public get text(): string { - return this.token.text; - } - - /** - * Returns a string representation of this object. - */ - public toString(): string { - return `prompt-reference/${this.type}:${this.subtype}/${this.token}`; - } -} - -/** - * A tiny utility object that helps us to track existence - * of at least one parse result from the content provider. - */ -class FirstParseResult extends DeferredPromise { - /** - * Private attribute to track if we have - * received at least one result. - */ - private _gotResult = false; - - /** - * Whether we've received at least one result. - */ - public get gotFirstResult(): boolean { - return this._gotResult; - } - - /** - * Get underlying promise reference. - */ - public get promise(): Promise { - return this.p; - } - - /** - * Complete the underlying promise. - */ - public end(): void { - this._gotResult = true; - super.complete(void 0) - .catch(() => { - // the complete method is never fails - // so we can ignore the error here - }); - - return; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts deleted file mode 100644 index c73efe10884..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/filePromptParser.ts +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI } from '../../../../../../base/common/uri.js'; -import { ILogService } from '../../../../../../platform/log/common/log.js'; -import { BasePromptParser, IPromptParserOptions } from './basePromptParser.js'; -import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; - -/** - * Class capable of parsing prompt syntax out of a provided file, - * including all the nested child file references it may have. - */ -export class FilePromptParser extends BasePromptParser { - constructor( - uri: URI, - options: IPromptParserOptions, - @IInstantiationService instantiationService: IInstantiationService, - @IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService, - @ILogService logService: ILogService, - ) { - const contentsProvider = instantiationService.createInstance(FilePromptContentProvider, uri, options); - super(contentsProvider, options, instantiationService, envService, logService); - - this._register(contentsProvider); - } - - /** - * Returns a string representation of this object. - */ - public override toString(): string { - return `file-prompt:${this.uri.path}`; - } -} 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 deleted file mode 100644 index 754293f6dcf..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/diagnostics.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 { 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/headerBase.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/headerBase.ts deleted file mode 100644 index eedebeabeed..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/headerBase.ts +++ /dev/null @@ -1,264 +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 { type TModeMetadata } from './modeHeader.js'; -import { localize } from '../../../../../../../nls.js'; -import { type TPromptMetadata } from './promptHeader.js'; -import { type IMetadataRecord } from './metadata/base/record.js'; -import { type TInstructionsMetadata } from './instructionsHeader.js'; -import { Range } from '../../../../../../../editor/common/core/range.js'; -import { Disposable } from '../../../../../../../base/common/lifecycle.js'; -import { ObjectStream } from '../../codecs/base/utils/objectStream.js'; -import { PromptMetadataError, PromptMetadataWarning, type TDiagnostic } from './diagnostics.js'; -import { SimpleToken } from '../../codecs/base/simpleCodec/tokens/tokens.js'; -import { FrontMatterRecord } from '../../codecs/base/frontMatterCodec/tokens/index.js'; -import { FrontMatterHeader } from '../../codecs/base/markdownExtensionsCodec/tokens/frontMatterHeader.js'; -import { FrontMatterDecoder, type TFrontMatterToken } from '../../codecs/base/frontMatterCodec/frontMatterDecoder.js'; -import { PromptDescriptionMetadata } from './metadata/description.js'; - -/** - * A metadata utility class "dehydrated" into a plain data object with - * semi-primitive record values (string, boolean, string[], boolean[], etc.). - */ -export type TDehydrated = { - [K in keyof T]: T[K] extends IMetadataRecord ? (U extends undefined ? undefined : NonNullable) : undefined; -}; - -/** - * Metadata defined in the prompt header. - */ -export interface IHeaderMetadata { - /** - * Description metadata in the prompt header. - */ - description: PromptDescriptionMetadata; -} - -/** - * Metadata for prompt/instruction/mode files. - */ -export type THeaderMetadata = Partial>; - -/** - * Metadata defined in the header of prompt/instruction/mode files. - */ -export type TMetadata = TPromptMetadata | TModeMetadata | TInstructionsMetadata; - -/** - * Base class for prompt/instruction/mode headers. - */ -export abstract class HeaderBase< - TMetadata extends IHeaderMetadata, -> extends Disposable { - /** - * Underlying decoder for a Front Matter header. - */ - private readonly stream: FrontMatterDecoder; - - /** - * Metadata records. - */ - protected readonly meta: Partial; - - /** - * Data object with all header's metadata records. - */ - public get metadata(): Partial> { - const result: Partial> = {}; - - for (const [entryName, entryValue] of Object.entries(this.meta)) { - if (entryValue?.value === undefined) { - continue; - } - - // note! we have to resort to `Object.assign()` here because - // the `Object.entries()` call looses type information - Object.assign(result, { - [entryName]: entryValue.value, - }); - } - - return result; - } - - /** - * A copy of metadata object with utility classes as values - * for each of prompt header's record. - * - * Please use {@link metadata} instead if all you need to read is - * the plain "data" object representation of valid metadata records. - */ - public get metadataUtility(): Partial { - return { ...this.meta }; - } - - /** - * List of all unique metadata record names. - */ - private readonly recordNames: Set; - - /** - * List of all issues found while parsing the prompt header. - */ - protected readonly issues: TDiagnostic[]; - - /** - * List of all diagnostic issues found while parsing - * the prompt header. - */ - public get diagnostics(): readonly TDiagnostic[] { - return this.issues; - } - - /** - * Full range of the header in the original document. - */ - public get range(): Range { - return this.token.range; - } - - constructor( - public readonly token: FrontMatterHeader, - public readonly languageId: string, - ) { - super(); - - this.issues = []; - this.meta = {}; - this.recordNames = new Set(); - - this.stream = this._register( - new FrontMatterDecoder( - ObjectStream.fromArray([...token.contentToken.children]), - ), - ); - this.stream.onData(this.onData.bind(this)); - this.stream.onError(this.onError.bind(this)); - } - - /** - * Process a front matter record token, which includes: - * - validation of the record and whether it is compatible with other header records - * - adding validation-related diagnostic messages to the {@link issues} list - * - setting associated utility class for the record on the {@link meta} object - * - * @returns a boolean flag that indicates whether the token was handled and therefore - * should not be processed any further. - */ - protected abstract handleToken( - token: FrontMatterRecord, - ): boolean; - - /** - * 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 property '{0}' will be ignored.", - recordName, - ), - ), - ); - - return; - } - this.recordNames.add(recordName); - - // if the record might be a "description" metadata - // add it to the list of parsed metadata records - if (PromptDescriptionMetadata.isDescriptionRecord(token)) { - const metadata = new PromptDescriptionMetadata(token, this.languageId); - - this.issues.push(...metadata.validate()); - this.meta.description = metadata; - this.recordNames.add(recordName); - return; - } - - // pipe the token to the actual implementation class - // that might to handle it based on the token type - if (this.handleToken(token)) { - return; - } - - // all other records are "unknown" ones - this.issues.push( - new PromptMetadataWarning( - token.range, - localize( - 'prompt.header.metadata.diagnostics.unknown-record', - "Unknown property '{0}' will be ignored.", - recordName, - ), - ), - ); - } - - /** - * Process errors from the underlying front matter decoder. - */ - private onError(error: Error): void { - this.issues.push( - new PromptMetadataError( - this.token.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/instructionsHeader.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/instructionsHeader.ts deleted file mode 100644 index 1f13901f960..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/instructionsHeader.ts +++ /dev/null @@ -1,44 +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 { PromptApplyToMetadata } from './metadata/applyTo.js'; -import { HeaderBase, IHeaderMetadata, type TDehydrated } from './headerBase.js'; -import { PromptsType } from '../../promptTypes.js'; -import { FrontMatterRecord } from '../../codecs/base/frontMatterCodec/tokens/index.js'; - -/** - * Metadata utility object for instruction files. - */ -interface IInstructionsMetadata extends IHeaderMetadata { - /** - * Chat 'applyTo' metadata in the prompt header. - */ - applyTo: PromptApplyToMetadata; -} - -/** - * Metadata for instruction files. - */ -export type TInstructionsMetadata = Partial> & { promptType: PromptsType.instructions }; - -/** - * Header object for instruction files. - */ -export class InstructionsHeader extends HeaderBase { - protected override handleToken(token: FrontMatterRecord): boolean { - // if the record might be a "applyTo" metadata - // add it to the list of parsed metadata records - if (PromptApplyToMetadata.isApplyToRecord(token)) { - const metadata = new PromptApplyToMetadata(token, this.languageId); - - this.issues.push(...metadata.validate()); - this.meta.applyTo = metadata; - - return true; - } - - return false; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/applyTo.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/applyTo.ts deleted file mode 100644 index 7f374c0f108..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/applyTo.ts +++ /dev/null @@ -1,122 +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 { PromptStringMetadata } from './base/string.js'; -import { localize } from '../../../../../../../../nls.js'; -import { INSTRUCTIONS_LANGUAGE_ID } from '../../../promptTypes.js'; -import { isEmptyPattern, parse, splitGlobAware } from '../../../../../../../../base/common/glob.js'; -import { PromptMetadataDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../diagnostics.js'; -import { FrontMatterRecord, FrontMatterToken } from '../../../codecs/base/frontMatterCodec/tokens/index.js'; - -/** - * Name of the metadata record in the prompt header. - */ -const RECORD_NAME = 'applyTo'; - -/** - * Prompt `applyTo` metadata record inside the prompt header. - */ -export class PromptApplyToMetadata extends PromptStringMetadata { - constructor( - recordToken: FrontMatterRecord, - languageId: string, - ) { - super(RECORD_NAME, recordToken, languageId); - } - - public override get recordName(): string { - return RECORD_NAME; - } - - public override validate(): readonly PromptMetadataDiagnostic[] { - super.validate(); - - // if we don't have a value token, validation must - // has failed already so nothing to do more - if (this.valueToken === undefined) { - return this.issues; - } - - // the applyTo metadata makes sense only for 'instruction' prompts - if (this.languageId !== INSTRUCTIONS_LANGUAGE_ID) { - this.issues.push( - new PromptMetadataError( - this.range, - localize( - 'prompt.header.metadata.string.diagnostics.invalid-language', - "The '{0}' header property is only valid in instruction files.", - this.recordName, - ), - ), - ); - - delete this.valueToken; - return this.issues; - } - - const { cleanText } = this.valueToken; - - // warn user if specified glob pattern is not valid - if (this.isValidGlob(cleanText) === false) { - this.issues.push( - new PromptMetadataWarning( - this.valueToken.range, - localize( - 'prompt.header.metadata.applyTo.diagnostics.non-valid-glob', - "Invalid glob pattern '{0}'.", - cleanText, - ), - ), - ); - - delete this.valueToken; - return this.issues; - } - - return this.issues; - } - - /** - * Check if a provided string contains a valid glob pattern. - */ - private isValidGlob( - pattern: string, - ): boolean { - try { - const patterns = splitGlobAware(pattern, ','); - if (patterns.length === 0) { - return false; - } - for (const pattern of patterns) { - - const globPattern = parse(pattern); - if (isEmptyPattern(globPattern)) { - return false; - } - } - return true; - } catch (_error) { - return false; - } - } - - /** - * Check if a provided front matter token is a metadata record - * with name equal to `applyTo`. - */ - public static isApplyToRecord( - token: FrontMatterToken, - ): boolean { - if ((token instanceof FrontMatterRecord) === false) { - return false; - } - - if (token.nameToken.text === RECORD_NAME) { - return true; - } - - return false; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/base/enum.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/base/enum.ts deleted file mode 100644 index 71b7d9e7a4f..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/base/enum.ts +++ /dev/null @@ -1,84 +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 { PromptStringMetadata } from './string.js'; -import { localize } from '../../../../../../../../../nls.js'; -import { assert } from '../../../../../../../../../base/common/assert.js'; -import { isOneOf } from '../../../../../../../../../base/common/types.js'; -import { PromptMetadataDiagnostic, PromptMetadataError } from '../../diagnostics.js'; -import { FrontMatterSequence } from '../../../../codecs/base/frontMatterCodec/tokens/frontMatterSequence.js'; -import { FrontMatterRecord, FrontMatterString } from '../../../../codecs/base/frontMatterCodec/tokens/index.js'; - -/** - * Enum type is the special case of the {@link PromptStringMetadata string} - * type that can take only a well-defined set of {@link validValues}. - */ -export abstract class PromptEnumMetadata< - TValidValues extends string = string, -> extends PromptStringMetadata { - constructor( - private readonly validValues: readonly TValidValues[], - expectedRecordName: string, - recordToken: FrontMatterRecord, - languageId: string, - ) { - super(expectedRecordName, recordToken, languageId); - } - - /** - * Valid enum value or 'undefined'. - */ - private enumValue: TValidValues | undefined; - /** - * Valid enum value or 'undefined'. - */ - public override get value(): TValidValues | undefined { - return this.enumValue; - } - - /** - * Validate the metadata record has an allowed value. - */ - public override validate(): readonly PromptMetadataDiagnostic[] { - super.validate(); - - if (this.valueToken === undefined) { - return this.issues; - } - - // sanity check for our expectations about the validate call - assert( - this.valueToken instanceof FrontMatterString - || this.valueToken instanceof FrontMatterSequence, - `Record token must be 'string', got '${this.valueToken}'.`, - ); - - const { cleanText } = this.valueToken; - if (isOneOf(cleanText, this.validValues)) { - this.enumValue = cleanText; - - return this.issues; - } - - this.issues.push( - new PromptMetadataError( - this.valueToken.range, - localize( - 'prompt.header.metadata.enum.diagnostics.invalid-value', - "The property '{0}' must be one of {1}, got '{2}'.", - this.recordName, - this.validValues - .map((value) => { - return `'${value}'`; - }).join(' | '), - cleanText, - ), - ), - ); - - delete this.valueToken; - return this.issues; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/base/record.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/base/record.ts deleted file mode 100644 index aa7a4660f66..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/base/record.ts +++ /dev/null @@ -1,108 +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 { assert } from '../../../../../../../../../base/common/assert.js'; -import { Range } from '../../../../../../../../../editor/common/core/range.js'; -import { PromptMetadataDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../../diagnostics.js'; -import { FrontMatterRecord } from '../../../../codecs/base/frontMatterCodec/tokens/index.js'; - -/** - * Supported primitive types for metadata values in a prompt header. - */ -type TMetadataPrimitive = string | boolean; - -/** - * Supported metadata values in a prompt header. - */ -type TMetadataValue = TMetadataPrimitive | TMetadataPrimitive[]; - -/** - * Interface for a generic metadata record in the prompt header. - */ -export interface IMetadataRecord { - /** - * Value of a metadata record. If the value is not defined, it usually - * means that a record is present but its value is not set or valid. - */ - readonly value: T | undefined; -} - -/** - * Abstract class for all metadata records in the prompt header. - */ -export abstract class PromptMetadataRecord implements IMetadataRecord { - /** - * Private field for tracking all diagnostic issues - * related to this metadata record. - */ - protected readonly issues: PromptMetadataDiagnostic[]; - - /** - * Full range of the metadata's record text in the prompt header. - */ - public get range(): Range { - return this.recordToken.range; - } - - constructor( - protected readonly expectedRecordName: string, - protected readonly recordToken: FrontMatterRecord, - protected readonly languageId: string, - ) { - // validate that the record name has the expected name - const recordName = recordToken.nameToken.text; - assert( - recordName === expectedRecordName, - `Record name must be '${expectedRecordName}', got '${recordName}'.`, - ); - - this.issues = []; - } - - /** - * Name of the metadata record. - */ - public get recordName(): string { - return this.recordToken.nameToken.text; - } - - /** - * Validate the metadata record and collect all issues - * related to its content. - */ - public abstract validate(): readonly PromptMetadataDiagnostic[]; - - /** - * List of all diagnostic issues related to this metadata record. - */ - public get diagnostics(): readonly PromptMetadataDiagnostic[] { - return this.issues; - } - - /** - * Get the value of the metadata record. - */ - public abstract get value(): TValue | undefined; - - /** - * 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/base/string.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/base/string.ts deleted file mode 100644 index 87423ab25b2..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/base/string.ts +++ /dev/null @@ -1,73 +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 { PromptMetadataRecord } from './record.js'; -import { localize } from '../../../../../../../../../nls.js'; -import { PromptMetadataDiagnostic, PromptMetadataError } from '../../diagnostics.js'; -import { FrontMatterSequence } from '../../../../codecs/base/frontMatterCodec/tokens/frontMatterSequence.js'; -import { FrontMatterRecord, FrontMatterString } from '../../../../codecs/base/frontMatterCodec/tokens/index.js'; -import { Range } from '../../../../../../../../../editor/common/core/range.js'; - - -/** - * Base class for all metadata records with a `string` value. - */ -export abstract class PromptStringMetadata extends PromptMetadataRecord { - /** - * Value token reference of the record. - */ - protected valueToken: FrontMatterString | FrontMatterSequence | undefined; - - /** - * String value of a metadata record. - */ - public override get value(): string | undefined { - return this.valueToken?.cleanText; - } - - public get valueRange(): Range | undefined { - return this.valueToken?.range; - } - - constructor( - expectedRecordName: string, - recordToken: FrontMatterRecord, - languageId: string, - ) { - super(expectedRecordName, recordToken, languageId); - } - - /** - * Validate the metadata record has a 'string' value. - */ - public override validate(): readonly PromptMetadataDiagnostic[] { - const { valueToken } = this.recordToken; - - // validate that the record value is a string or a generic sequence - // of tokens that can be interpreted as a string without quotes - const isString = (valueToken instanceof FrontMatterString); - const isSequence = (valueToken instanceof FrontMatterSequence); - if (isString || isSequence) { - this.valueToken = valueToken; - return this.issues; - } - - this.issues.push( - new PromptMetadataError( - valueToken.range, - localize( - 'prompt.header.metadata.string.diagnostics.invalid-value-type', - "The property '{0}' must be of type '{1}', got '{2}'.", - this.recordName, - 'string', - valueToken.valueTypeName.toString(), - ), - ), - ); - - delete this.valueToken; - return this.issues; - } -} 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 deleted file mode 100644 index aaa8fa3a640..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/description.ts +++ /dev/null @@ -1,46 +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 { PromptStringMetadata } from './base/string.js'; -import { FrontMatterRecord, FrontMatterToken } from '../../../codecs/base/frontMatterCodec/tokens/index.js'; - -/** - * Name of the metadata record in the prompt header. - */ -const RECORD_NAME = 'description'; - -/** - * Prompt `description` metadata record inside the prompt header. - */ -export class PromptDescriptionMetadata extends PromptStringMetadata { - public override get recordName(): string { - return RECORD_NAME; - } - - constructor( - recordToken: FrontMatterRecord, - languageId: string, - ) { - super(RECORD_NAME, recordToken, languageId); - } - - /** - * Check if a provided front matter token is a metadata record - * with name equal to `description`. - */ - public static isDescriptionRecord( - token: FrontMatterToken, - ): boolean { - if ((token instanceof FrontMatterRecord) === false) { - return false; - } - - if (token.nameToken.text === RECORD_NAME) { - return true; - } - - return false; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.ts deleted file mode 100644 index b12728f7e75..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/mode.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 { PromptStringMetadata } from './base/string.js'; -import { FrontMatterRecord, FrontMatterToken } from '../../../codecs/base/frontMatterCodec/tokens/index.js'; - -/** - * Name of the metadata record in the prompt header. - */ -const RECORD_NAME = 'mode'; - -/** - * Prompt `mode` metadata record inside the prompt header. - * Now supports both built-in modes (ask, edit, agent) and custom mode IDs. - */ -export class PromptModeMetadata extends PromptStringMetadata { - constructor( - recordToken: FrontMatterRecord, - languageId: string, - ) { - super( - RECORD_NAME, - recordToken, - languageId, - ); - } - - /** - * Check if a provided front matter token is a metadata record - * with name equal to `mode`. - */ - public static isModeRecord( - token: FrontMatterToken, - ): boolean { - if ((token instanceof FrontMatterRecord) === false) { - return false; - } - - if (token.nameToken.text === RECORD_NAME) { - return true; - } - - return false; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/model.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/model.ts deleted file mode 100644 index aa559e57fc0..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/model.ts +++ /dev/null @@ -1,41 +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 { FrontMatterRecord, FrontMatterToken } from '../../../codecs/base/frontMatterCodec/tokens/index.js'; -import { PromptStringMetadata } from './base/string.js'; - -/** - * Name of the metadata record in the prompt header. - */ -const RECORD_NAME = 'model'; - -export class PromptModelMetadata extends PromptStringMetadata { - public override get recordName(): string { - return RECORD_NAME; - } - - constructor( - recordToken: FrontMatterRecord, - languageId: string, - ) { - super(RECORD_NAME, recordToken, languageId); - } - - /** - * Check if a provided front matter token is a metadata record - * with name equal to `description`. - */ - public static isModelRecord(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/tools.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts deleted file mode 100644 index ad52e5faebe..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/metadata/tools.ts +++ /dev/null @@ -1,182 +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 { PromptMetadataRecord } from './base/record.js'; -import { localize } from '../../../../../../../../nls.js'; -import { PromptMetadataDiagnostic, PromptMetadataError, PromptMetadataWarning } from '../diagnostics.js'; -import { FrontMatterSequence } from '../../../codecs/base/frontMatterCodec/tokens/frontMatterSequence.js'; -import { FrontMatterArray, FrontMatterRecord, FrontMatterString, FrontMatterToken, FrontMatterValueToken } from '../../../codecs/base/frontMatterCodec/tokens/index.js'; -import { Range } from '../../../../../../../../editor/common/core/range.js'; - -/** - * Name of the metadata record in the prompt header. - */ -const RECORD_NAME = 'tools'; - -/** - * Prompt `tools` metadata record inside the prompt header. - */ -export class PromptToolsMetadata extends PromptMetadataRecord { - - /** - * List of all valid tool names that were found in - * this metadata record. - */ - public override get value(): string[] | undefined { - if (this.validToolNames === undefined) { - return []; - } - - return [...this.validToolNames.keys()]; - } - - public override get recordName(): string { - return RECORD_NAME; - } - - /** - * Value token reference of the record. - */ - protected valueToken: FrontMatterArray | undefined; - - /** - * List of all valid tool names that were found in - * this metadata record. - */ - private validToolNames: Map | undefined; - - - - constructor( - recordToken: FrontMatterRecord, - languageId: string, - ) { - super(RECORD_NAME, recordToken, languageId); - } - - /** - * Validate the metadata record and collect all issues - * related to its content. - */ - public override validate(): readonly PromptMetadataDiagnostic[] { - 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', - "Must be an array of tool names, got '{0}'.", - valueToken.valueTypeName.toString(), - ), - ), - ); - - delete this.valueToken; - return this.issues; - } - - this.valueToken = valueToken; - - // validate that all array items - this.validToolNames = new Map(); - for (const item of this.valueToken.items) { - this.issues.push( - ...this.validateToolName(item, this.validToolNames), - ); - } - - return this.issues; - } - - public getToolRange(toolName: string): Range | undefined { - return this.validToolNames?.get(toolName); - } - - /** - * Validate an individual provided value token that is used - * for a tool name. - */ - private validateToolName( - valueToken: FrontMatterValueToken, - validToolNames: Map, - ): readonly PromptMetadataDiagnostic[] { - const issues: PromptMetadataDiagnostic[] = []; - - // tool name must be a quoted or an unquoted 'string' - if ( - (valueToken instanceof FrontMatterString) === false && - (valueToken instanceof FrontMatterSequence) === false - ) { - issues.push( - new PromptMetadataWarning( - valueToken.range, - localize( - 'prompt.header.metadata.tools.diagnostics.invalid-tool-name-type', - "Unexpected tool name '{0}', expected a string literal.", - valueToken.text - ), - ), - ); - - return issues; - } - - const cleanToolName = valueToken.cleanText.trim(); - // the tool name should not be empty - if (cleanToolName.length === 0) { - issues.push( - new PromptMetadataWarning( - valueToken.range, - localize( - 'prompt.header.metadata.tools.diagnostics.empty-tool-name', - "Tool name cannot be empty.", - ), - ), - ); - - return issues; - } - - // the tool name should not be duplicated - if (validToolNames.has(cleanToolName)) { - issues.push( - new PromptMetadataWarning( - valueToken.range, - localize( - 'prompt.header.metadata.tools.diagnostics.duplicate-tool-name', - "Duplicate tool name '{0}'.", - cleanToolName, - ), - ), - ); - - return issues; - } - - validToolNames.set(cleanToolName, valueToken.range); - return issues; - } - - /** - * Check if a provided front matter token is a metadata record - * with name equal to `tools`. - */ - public static isToolsRecord( - token: FrontMatterToken, - ): boolean { - if ((token instanceof FrontMatterRecord) === false) { - return false; - } - - if (token.nameToken.text === RECORD_NAME) { - return true; - } - - return false; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/modeHeader.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/modeHeader.ts deleted file mode 100644 index 94307163bf6..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/modeHeader.ts +++ /dev/null @@ -1,56 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { HeaderBase, IHeaderMetadata, type TDehydrated } from './headerBase.js'; -import { PromptsType } from '../../promptTypes.js'; -import { FrontMatterRecord } from '../../codecs/base/frontMatterCodec/tokens/index.js'; -import { PromptModelMetadata } from './metadata/model.js'; -import { PromptToolsMetadata } from './metadata/tools.js'; - -/** - * Metadata utility object for mode files. - */ -interface IModeMetadata extends IHeaderMetadata { - /** - * Tools metadata in the mode header. - */ - tools: PromptToolsMetadata; - - /** - * Chat model metadata in the mode header. - */ - model: PromptModelMetadata; -} - -/** - * Metadata for mode files. - */ -export type TModeMetadata = Partial> & { promptType: PromptsType.mode }; - -/** - * Header object for mode files. - */ -export class ModeHeader extends HeaderBase { - protected override handleToken(token: FrontMatterRecord): boolean { - // if the record might be a "tools" metadata - // add it to the list of parsed metadata records - if (PromptToolsMetadata.isToolsRecord(token)) { - const metadata = new PromptToolsMetadata(token, this.languageId); - - this.issues.push(...metadata.validate()); - this.meta.tools = metadata; - return true; - } - if (PromptModelMetadata.isModelRecord(token)) { - const metadata = new PromptModelMetadata(token, this.languageId); - - this.issues.push(...metadata.validate()); - this.meta.model = metadata; - - return true; - } - return false; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/promptHeader.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/promptHeader.ts deleted file mode 100644 index 250e8382372..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptHeader/promptHeader.ts +++ /dev/null @@ -1,103 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ChatModeKind } from '../../../constants.js'; -import { localize } from '../../../../../../../nls.js'; -import { PromptMetadataWarning } from './diagnostics.js'; -import { HeaderBase, IHeaderMetadata, type TDehydrated } from './headerBase.js'; -import { PromptsType } from '../../promptTypes.js'; -import { FrontMatterRecord } from '../../codecs/base/frontMatterCodec/tokens/index.js'; -import { PromptModelMetadata } from './metadata/model.js'; -import { PromptToolsMetadata } from './metadata/tools.js'; -import { PromptModeMetadata } from './metadata/mode.js'; - -/** - * Metadata utility object for prompt files. - */ -export interface IPromptMetadata extends IHeaderMetadata { - /** - * Tools metadata in the prompt header. - */ - tools: PromptToolsMetadata; - - /** - * Chat mode metadata in the prompt header. - */ - mode: PromptModeMetadata; - - /** - * Chat model metadata in the prompt header. - */ - model: PromptModelMetadata; -} - -/** - * Metadata for prompt files. - */ -export type TPromptMetadata = Partial> & { promptType: PromptsType.prompt }; - -/** - * Header object for prompt files. - */ -export class PromptHeader extends HeaderBase { - protected override handleToken(token: FrontMatterRecord): boolean { - // if the record might be a "tools" metadata - // add it to the list of parsed metadata records - if (PromptToolsMetadata.isToolsRecord(token)) { - const metadata = new PromptToolsMetadata(token, this.languageId); - - this.issues.push(...metadata.validate()); - this.meta.tools = metadata; - - this.validateToolsAndModeCompatibility(); - return true; - } - - // if the record might be a "mode" metadata - // add it to the list of parsed metadata records - if (PromptModeMetadata.isModeRecord(token)) { - const metadata = new PromptModeMetadata(token, this.languageId); - - this.issues.push(...metadata.validate()); - this.meta.mode = metadata; - - this.validateToolsAndModeCompatibility(); - return true; - } - - if (PromptModelMetadata.isModelRecord(token)) { - const metadata = new PromptModelMetadata(token, this.languageId); - - this.issues.push(...metadata.validate()); - this.meta.model = metadata; - - return true; - } - - return false; - } - - /** - * Validate that the `tools` and `mode` metadata are compatible - * with each other. If not, add a warning diagnostic. - */ - private validateToolsAndModeCompatibility(): void { - const { tools, mode } = this.meta; - const modeValue = mode?.value; - - if (tools !== undefined && (modeValue === ChatModeKind.Edit || modeValue === ChatModeKind.Ask)) { - this.issues.push( - new PromptMetadataWarning( - tools.range, - localize( - 'prompt.header.metadata.mode.diagnostics.incompatible-with-tools', - "Tools can not be used in '{0}' mode and will be ignored.", - modeValue - ), - ), - ); - } - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptParser.ts deleted file mode 100644 index 99df26fd5e9..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/promptParser.ts +++ /dev/null @@ -1,72 +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 { URI } from '../../../../../../base/common/uri.js'; -import { IPromptContentsProvider } from '../contentProviders/types.js'; -import { ILogService } from '../../../../../../platform/log/common/log.js'; -import { BasePromptParser, IPromptParserOptions } from './basePromptParser.js'; -import { IModelService } from '../../../../../../editor/common/services/model.js'; -import { TextModelContentsProvider } from '../contentProviders/textModelContentsProvider.js'; -import { FilePromptContentProvider } from '../contentProviders/filePromptContentsProvider.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IPromptContentsProviderOptions } from '../contentProviders/promptContentsProviderBase.js'; -import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; - -/** - * Get prompt contents provider object based on the prompt type. - */ -function getContentsProvider( - uri: URI, - options: IPromptContentsProviderOptions, - modelService: IModelService, - instaService: IInstantiationService -): IPromptContentsProvider { - const model = modelService.getModel(uri); - if (model) { - return instaService.createInstance(TextModelContentsProvider, model, options); - } - return instaService.createInstance(FilePromptContentProvider, uri, options); -} - -/** - * General prompt parser class that automatically infers a prompt - * contents provider type by the type of provided prompt URI. - */ -export class PromptParser extends BasePromptParser { - /** - * Underlying prompt contents provider instance. - */ - private readonly contentsProvider: IPromptContentsProvider; - - constructor( - uri: URI, - options: IPromptParserOptions, - @ILogService logService: ILogService, - @IModelService modelService: IModelService, - @IInstantiationService instaService: IInstantiationService, - @IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService, - ) { - const contentsProvider = getContentsProvider(uri, options, modelService, instaService); - - super( - contentsProvider, - options, - instaService, - envService, - logService, - ); - - this.contentsProvider = this._register(contentsProvider); - } - - /** - * Returns a string representation of this object. - */ - public override toString(): string { - const { sourceName } = this.contentsProvider; - - return `prompt-parser:${sourceName}:${this.uri.path}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts deleted file mode 100644 index bb2b7060abb..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/textModelPromptParser.ts +++ /dev/null @@ -1,42 +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 { ITextModel } from '../../../../../../editor/common/model.js'; -import { ILogService } from '../../../../../../platform/log/common/log.js'; -import { BasePromptParser, IPromptParserOptions } from './basePromptParser.js'; -import { TextModelContentsProvider } from '../contentProviders/textModelContentsProvider.js'; -import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; - -/** - * Class capable of parsing prompt syntax out of a provided text model, - * including all the nested child file references it may have. - */ -export class TextModelPromptParser extends BasePromptParser { - constructor( - model: ITextModel, - options: IPromptParserOptions, - @IInstantiationService instantiationService: IInstantiationService, - @IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService, - @ILogService logService: ILogService, - ) { - const contentsProvider = instantiationService.createInstance( - TextModelContentsProvider, - model, - options, - ); - - super(contentsProvider, options, instantiationService, envService, logService); - - this._register(contentsProvider); - } - - /** - * Returns a string representation of this object. - */ - public override toString(): string { - return `text-model-prompt:${this.uri.path}`; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/topError.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/topError.ts deleted file mode 100644 index b6281d3f42b..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/topError.ts +++ /dev/null @@ -1,102 +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 { ITopError } from './types.js'; -import { localize } from '../../../../../../nls.js'; -import { assert } from '../../../../../../base/common/assert.js'; -import { assertDefined } from '../../../../../../base/common/types.js'; -import { OpenFailed, RecursiveReference, FailedToResolveContentsStream } from '../../promptFileReferenceErrors.js'; - -/** - * The top-most error of the reference tree. - */ -export class TopError implements ITopError { - public readonly originalError: ITopError['originalError']; - public readonly errorSubject: ITopError['errorSubject']; - public readonly errorsCount: ITopError['errorsCount']; - public readonly parentUri: ITopError['parentUri']; - - constructor( - options: Omit, - ) { - this.originalError = options.originalError; - this.errorSubject = options.errorSubject; - this.errorsCount = options.errorsCount; - this.parentUri = options.parentUri; - } - - public get localizedMessage(): string { - const { originalError, parentUri, errorSubject: subject, errorsCount } = this; - - assert( - errorsCount >= 1, - `Error count must be at least 1, got '${errorsCount}'.`, - ); - - // a note about how many more link issues are there - const moreIssuesLabel = (errorsCount > 1) - ? localize('workbench.reusable-prompts.top-error.more-issues-label', "\n(+{0} more issues)", errorsCount - 1) - : ''; - - if (subject === 'root') { - if (originalError instanceof OpenFailed) { - return localize( - 'workbench.reusable-prompts.top-error.open-failed', - "Cannot open '{0}'.{1}", - originalError.uri.path, - moreIssuesLabel, - ); - } - - if (originalError instanceof FailedToResolveContentsStream) { - return localize( - 'workbench.reusable-prompts.top-error.cannot-read', - "Cannot read '{0}'.{1}", - originalError.uri.path, - moreIssuesLabel, - ); - } - - if (originalError instanceof RecursiveReference) { - return localize( - 'workbench.reusable-prompts.top-error.recursive-reference', - "Recursion to itself.", - ); - } - - return originalError.message + moreIssuesLabel; - } - - // a sanity check - because the error subject is not `root`, the parent must set - assertDefined( - parentUri, - 'Parent URI must be defined for error of non-root link.', - ); - - const errorMessageStart = (subject === 'child') - ? localize( - 'workbench.reusable-prompts.top-error.child.direct', - "Contains", - ) - : localize( - 'workbench.reusable-prompts.top-error.child.indirect', - "Indirectly referenced prompt '{0}' contains", - parentUri.path, - ); - - const linkIssueName = (originalError instanceof RecursiveReference) - ? localize('recursive', "recursive") - : localize('broken', "broken"); - - return localize( - 'workbench.reusable-prompts.top-error.child.final-message', - "{0} a {1} link to '{2}' that will be ignored.{3}", - errorMessageStart, - linkIssueName, - originalError.uri.path, - moreIssuesLabel, - ); - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts deleted file mode 100644 index 0a3d693c61d..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/parsers/types.ts +++ /dev/null @@ -1,116 +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 { URI } from '../../../../../../base/common/uri.js'; -import { ResolveError } from '../../promptFileReferenceErrors.js'; -import { IRange, Range } from '../../../../../../editor/common/core/range.js'; - -/** - * A resolve error with a parent prompt URI, if any. - */ -export interface IResolveError { - /** - * Original error instance. - */ - readonly originalError: ResolveError; - - /** - * URI of the parent that references this error. - */ - readonly parentUri?: URI; -} - -/** - * Top most error of the reference tree. - */ -export interface ITopError extends IResolveError { - /** - * Where does the error belong to: - * - * - `root` - the error is the top most error of the entire tree - * - `child` - the error is a child of the root error - * - `indirect-child` - the error is a child of a child of the root error - */ - readonly errorSubject: 'root' | 'child' | 'indirect-child'; - - /** - * Total number of all errors in the references tree, including the error - * of the current reference and all possible errors of its children. - */ - readonly errorsCount: number; - - /** - * Localized error message. - */ - readonly localizedMessage: string; -} - -/** - * Base interface for a generic prompt reference. - */ -interface IPromptReferenceBase { - /** - * Type of the prompt reference. E.g., `file`, `http`, `image`, etc. - */ - readonly type: string; - - /** - * Subtype of the prompt reference. For instance a `file` reference - * can be a `markdown link` or a prompt `#file:` variable reference. - */ - readonly subtype: string; - - /** - * URI component of the associated with this reference. - */ - readonly uri: URI; - - /** - * The full range of the prompt reference in the source text, - * including the {@link linkRange} and any additional - * parts the reference may contain (e.g., the `#file:` prefix). - */ - readonly range: Range; - - /** - * Range of the link part that the reference points to. - */ - readonly linkRange: IRange | undefined; - - /** - * Text of the reference as it appears in the source. - */ - readonly text: string; - - /** - * Original link path as it appears in the source. - */ - readonly path: string; - -} - -/** - * The special case of the {@link IPromptReferenceBase} that pertains - * to a file resource on the disk. - */ -export interface IPromptFileReference extends IPromptReferenceBase { - readonly type: 'file'; - - /** - * Subtype of a file reference, - either a prompt `#file` variable, - * or a `markdown link` (e.g., `[caption](/path/to/file.md)`). - */ - readonly subtype: 'prompt' | 'markdown'; -} - -/** - * List of all known prompt reference types. - */ -export type TPromptReference = IPromptFileReference; - -export type TVariableReference = { - readonly name: string; - readonly range: Range; -}; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts index 63c908abeeb..66cee497434 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileContributions.ts @@ -3,52 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ConfigMigration } from './config/configMigration.js'; -import { Registry } from '../../../../../platform/registry/common/platform.js'; -import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; -import { IWorkbenchContributionsRegistry, Extensions, IWorkbenchContribution } from '../../../../common/contributions.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { PromptLinkProvider } from './languageProviders/promptLinkProvider.js'; -import { PromptLinkDiagnosticsInstanceManager } from './languageProviders/promptLinkDiagnosticsProvider.js'; -import { PromptHeaderDiagnosticsInstanceManager } from './languageProviders/promptHeaderDiagnosticsProvider.js'; import { PromptBodyAutocompletion } from './languageProviders/promptBodyAutocompletion.js'; import { PromptHeaderAutocompletion } from './languageProviders/promptHeaderAutocompletion.js'; -import { PromptHeaderHoverProvider } from './languageProviders/promptHeaderHovers.js'; +import { PromptHoverProvider } from './languageProviders/promptHovers.js'; import { PromptHeaderDefinitionProvider } from './languageProviders/PromptHeaderDefinitionProvider.js'; +import { PromptValidatorContribution } from './languageProviders/promptValidator.js'; +import { PromptDocumentSemanticTokensProvider } from './languageProviders/promptDocumentSemanticTokensProvider.js'; +import { PromptCodeActionProvider } from './languageProviders/promptCodeActions.js'; +import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { ALL_PROMPTS_LANGUAGE_SELECTOR } from './promptTypes.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +export class PromptLanguageFeaturesProvider extends Disposable implements IWorkbenchContribution { + static readonly ID = 'chat.promptLanguageFeatures'; -/** - * Function that registers all prompt-file related contributions. - */ -export function registerPromptFileContributions(): void { + constructor( + @ILanguageFeaturesService languageService: ILanguageFeaturesService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); - // all language constributions + this._register(languageService.linkProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, instantiationService.createInstance(PromptLinkProvider))); + this._register(languageService.completionProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, instantiationService.createInstance(PromptBodyAutocompletion))); + this._register(languageService.completionProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, instantiationService.createInstance(PromptHeaderAutocompletion))); + this._register(languageService.hoverProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, instantiationService.createInstance(PromptHoverProvider))); + this._register(languageService.definitionProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, instantiationService.createInstance(PromptHeaderDefinitionProvider))); + this._register(languageService.documentSemanticTokensProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, instantiationService.createInstance(PromptDocumentSemanticTokensProvider))); + this._register(languageService.codeActionProvider.register(ALL_PROMPTS_LANGUAGE_SELECTOR, instantiationService.createInstance(PromptCodeActionProvider))); - registerContribution(PromptLinkProvider); - registerContribution(PromptLinkDiagnosticsInstanceManager); - registerContribution(PromptHeaderDiagnosticsInstanceManager); - /** - * PromptDecorationsProviderInstanceManager is currently disabled because the only currently - * available decoration is the Front Matter header, which we decided to disable for now. - * Add it back when more decorations are needed. - */ - // registerContribution(PromptDecorationsProviderInstanceManager); , - - - registerContribution(PromptBodyAutocompletion); - registerContribution(PromptHeaderAutocompletion); - registerContribution(PromptHeaderHoverProvider); - registerContribution(PromptHeaderDefinitionProvider); - registerContribution(ConfigMigration); -} - -/** - * Type for a generic workbench contribution. - */ -export type TContribution = new (...args: any[]) => IWorkbenchContribution; - -/** - * Register a specific workbench contribution. - */ -function registerContribution(contribution: TContribution): void { - Registry.as(Extensions.Workbench).registerWorkbenchContribution(contribution, LifecyclePhase.Eventually); + this._register(instantiationService.createInstance(PromptValidatorContribution)); + } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/newPromptsParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/newPromptsParser.ts new file mode 100644 index 00000000000..801e9338873 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/newPromptsParser.ts @@ -0,0 +1,308 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Iterable } from '../../../../../../base/common/iterator.js'; +import { dirname, joinPath } from '../../../../../../base/common/resources.js'; +import { splitLinesIncludeSeparators } from '../../../../../../base/common/strings.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { parse, YamlNode, YamlParseError, Position as YamlPosition } from '../../../../../../base/common/yaml.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { chatVariableLeader } from '../../chatParserTypes.js'; + +export class NewPromptsParser { + constructor() { + } + + public parse(uri: URI, content: string): ParsedPromptFile { + const linesWithEOL = splitLinesIncludeSeparators(content); + if (linesWithEOL.length === 0) { + return new ParsedPromptFile(uri, undefined, undefined); + } + let header: PromptHeader | undefined = undefined; + let body: PromptBody | undefined = undefined; + let bodyStartLine = 0; + if (linesWithEOL[0].match(/^---[\s\r\n]*$/)) { + let headerEndLine = linesWithEOL.findIndex((line, index) => index > 0 && line.match(/^---[\s\r\n]*$/)); + if (headerEndLine === -1) { + headerEndLine = linesWithEOL.length; + bodyStartLine = linesWithEOL.length; + } else { + bodyStartLine = headerEndLine + 1; + } + // range starts on the line after the ---, and ends at the beginning of the line that has the closing --- + const range = new Range(2, 1, headerEndLine + 1, 1); + header = new PromptHeader(range, linesWithEOL); + } + if (bodyStartLine < linesWithEOL.length) { + // range starts on the line after the ---, and ends at the beginning of line after the last line + const range = new Range(bodyStartLine + 1, 1, linesWithEOL.length + 1, 1); + body = new PromptBody(range, linesWithEOL, uri); + } + return new ParsedPromptFile(uri, header, body); + } +} + + +export class ParsedPromptFile { + constructor(public readonly uri: URI, public readonly header?: PromptHeader, public readonly body?: PromptBody) { + } +} + +export interface ParseError { + readonly message: string; + readonly range: Range; + readonly code: string; +} + +interface ParsedHeader { + readonly node: YamlNode | undefined; + readonly errors: ParseError[]; + readonly attributes: IHeaderAttribute[]; +} + +export class PromptHeader { + private _parsed: ParsedHeader | undefined; + + constructor(public readonly range: Range, private readonly linesWithEOL: string[]) { + } + + private get _parsedHeader(): ParsedHeader { + if (this._parsed === undefined) { + const yamlErrors: YamlParseError[] = []; + const lines = this.linesWithEOL.slice(this.range.startLineNumber - 1, this.range.endLineNumber - 1).join(''); + const node = parse(lines, yamlErrors); + const attributes = []; + const errors: ParseError[] = yamlErrors.map(err => ({ message: err.message, range: this.asRange(err), code: err.code })); + if (node?.type === 'object') { + for (const property of node.properties) { + attributes.push({ + key: property.key.value, + range: this.asRange({ start: property.key.start, end: property.value.end }), + value: this.asValue(property.value) + }); + } + } else { + errors.push({ message: 'Invalid header, expecting pairs', range: this.range, code: 'INVALID_YAML' }); + } + this._parsed = { node, attributes, errors }; + } + return this._parsed; + } + + private asRange({ start, end }: { start: YamlPosition; end: YamlPosition }): Range { + return new Range(this.range.startLineNumber + start.line, start.character + 1, this.range.startLineNumber + end.line, end.character + 1); + } + + private asValue(node: YamlNode): IValue { + switch (node.type) { + case 'string': + return { type: 'string', value: node.value, range: this.asRange(node) }; + case 'number': + return { type: 'number', value: node.value, range: this.asRange(node) }; + case 'boolean': + return { type: 'boolean', value: node.value, range: this.asRange(node) }; + case 'null': + return { type: 'null', value: node.value, range: this.asRange(node) }; + case 'array': + return { type: 'array', items: node.items.map(item => this.asValue(item)), range: this.asRange(node) }; + case 'object': { + const properties = node.properties.map(property => ({ key: this.asValue(property.key) as IStringValue, value: this.asValue(property.value) })); + return { type: 'object', properties, range: this.asRange(node) }; + } + } + } + + public get attributes(): IHeaderAttribute[] { + return this._parsedHeader.attributes; + } + + public getAttribute(key: string): IHeaderAttribute | undefined { + return this._parsedHeader.attributes.find(attr => attr.key === key); + } + + public get errors(): ParseError[] { + return this._parsedHeader.errors; + } + + private getStringAttribute(key: string): string | undefined { + const attribute = this._parsedHeader.attributes.find(attr => attr.key === key); + if (attribute?.value.type === 'string') { + return attribute.value.value; + } + return undefined; + } + + public get description(): string | undefined { + return this.getStringAttribute('description'); + } + + public get mode(): string | undefined { + return this.getStringAttribute('mode'); + } + + public get model(): string | undefined { + return this.getStringAttribute('model'); + } + + public get applyTo(): string | undefined { + return this.getStringAttribute('applyTo'); + } + + public get tools(): string[] | undefined { + const toolsAttribute = this._parsedHeader.attributes.find(attr => attr.key === 'tools'); + if (!toolsAttribute) { + return undefined; + } + if (toolsAttribute.value.type === 'array') { + const tools: string[] = []; + for (const item of toolsAttribute.value.items) { + if (item.type === 'string' && item.value) { + tools.push(item.value); + } + } + return tools; + } else if (toolsAttribute.value.type === 'object') { + const tools: string[] = []; + const collectLeafs = ({ key, value }: { key: IStringValue; value: IValue }) => { + if (value.type === 'boolean') { + tools.push(key.value); + } else if (value.type === 'object') { + value.properties.forEach(collectLeafs); + } + }; + toolsAttribute.value.properties.forEach(collectLeafs); + return tools; + } + return undefined; + } + +} + +export interface IHeaderAttribute { + readonly range: Range; + readonly key: string; + readonly value: IValue; +} + +export interface IStringValue { readonly type: 'string'; readonly value: string; readonly range: Range } +export interface INumberValue { readonly type: 'number'; readonly value: number; readonly range: Range } +export interface INullValue { readonly type: 'null'; readonly value: null; readonly range: Range } +export interface IBooleanValue { readonly type: 'boolean'; readonly value: boolean; readonly range: Range } + +export interface IArrayValue { + readonly type: 'array'; + readonly items: readonly IValue[]; + readonly range: Range; +} + +export interface IObjectValue { + readonly type: 'object'; + readonly properties: { key: IStringValue; value: IValue }[]; + readonly range: Range; +} + +export type IValue = IStringValue | INumberValue | IBooleanValue | IArrayValue | IObjectValue | INullValue; + + +interface ParsedBody { + readonly fileReferences: readonly IBodyFileReference[]; + readonly variableReferences: readonly IBodyVariableReference[]; + readonly bodyOffset: number; +} + +export class PromptBody { + private _parsed: ParsedBody | undefined; + + constructor(public readonly range: Range, private readonly linesWithEOL: string[], public readonly uri: URI) { + } + + public get fileReferences(): readonly IBodyFileReference[] { + return this.getParsedBody().fileReferences; + } + + public get variableReferences(): readonly IBodyVariableReference[] { + return this.getParsedBody().variableReferences; + } + + public get offset(): number { + return this.getParsedBody().bodyOffset; + } + + private getParsedBody(): ParsedBody { + if (this._parsed === undefined) { + const markdownLinkRanges: Range[] = []; + const fileReferences: IBodyFileReference[] = []; + const variableReferences: IBodyVariableReference[] = []; + const bodyOffset = Iterable.reduce(Iterable.slice(this.linesWithEOL, 0, this.range.startLineNumber - 1), (len, line) => line.length + len, 0); + for (let i = this.range.startLineNumber - 1, lineStartOffset = bodyOffset; i < this.range.endLineNumber - 1; i++) { + const line = this.linesWithEOL[i]; + const linkMatch = line.matchAll(/\[(.*?)\]\((.+?)\)/g); + for (const match of linkMatch) { + const linkEndOffset = match.index + match[0].length - 1; // before the parenthesis + const linkStartOffset = match.index + match[0].length - match[2].length - 1; + const range = new Range(i + 1, linkStartOffset + 1, i + 1, linkEndOffset + 1); + fileReferences.push({ content: match[2], range, isMarkdownLink: true }); + markdownLinkRanges.push(new Range(i + 1, match.index + 1, i + 1, match.index + match[0].length + 1)); + } + const reg = new RegExp(`${chatVariableLeader}([\\w]+:)?([^\\s#]+)`, 'g'); + const matches = line.matchAll(reg); + for (const match of matches) { + const fullRange = new Range(i + 1, match.index + 1, i + 1, match.index + match[0].length + 1); + if (markdownLinkRanges.some(mdRange => Range.areIntersectingOrTouching(mdRange, fullRange))) { + continue; + } + const varType = match[1]; + if (varType) { + if (varType === 'file:') { + const linkStartOffset = match.index + match[0].length - match[2].length; + const linkEndOffset = match.index + match[0].length; + const range = new Range(i + 1, linkStartOffset + 1, i + 1, linkEndOffset + 1); + fileReferences.push({ content: match[2], range, isMarkdownLink: false }); + } + } else { + const contentStartOffset = match.index + 1; // after the # + const contentEndOffset = match.index + match[0].length; + const range = new Range(i + 1, contentStartOffset + 1, i + 1, contentEndOffset + 1); + variableReferences.push({ name: match[2], range, offset: lineStartOffset + match.index }); + } + } + lineStartOffset += line.length; + } + this._parsed = { fileReferences: fileReferences.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)), variableReferences, bodyOffset }; + } + return this._parsed; + } + + public getContent(): string { + return this.linesWithEOL.slice(this.range.startLineNumber - 1, this.range.endLineNumber - 1).join(''); + } + + public resolveFilePath(path: string): URI | undefined { + try { + if (path.startsWith('/')) { + return this.uri.with({ path }); + } else if (path.match(/^[a-zA-Z]:\\/)) { + return URI.parse(path); + } else { + const dirName = dirname(this.uri); + return joinPath(dirName, path); + } + } catch { + return undefined; + } + } +} + +export interface IBodyFileReference { + readonly content: string; + readonly range: Range; + readonly isMarkdownLink: boolean; +} + +export interface IBodyVariableReference { + readonly name: string; + readonly range: Range; + readonly offset: number; +} 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 ba6751b7685..945eb7fa5f6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -6,15 +6,14 @@ import { ChatModeKind } from '../../constants.js'; import { URI } from '../../../../../../base/common/uri.js'; import { Event } from '../../../../../../base/common/event.js'; -import { TMetadata } from '../parsers/promptHeader/headerBase.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; import { IDisposable } from '../../../../../../base/common/lifecycle.js'; -import { TextModelPromptParser } from '../parsers/textModelPromptParser.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { PromptsType } from '../promptTypes.js'; import { createDecorator } from '../../../../../../platform/instantiation/common/instantiation.js'; -import { ITopError } from '../parsers/types.js'; -import { IVariableReference } from '../../chatModes.js'; +import { YamlNode, YamlParseError } from '../../../../../../base/common/yaml.js'; +import { IChatModeInstructions } from '../../chatModes.js'; +import { ParsedPromptFile } from './newPromptsParser.js'; /** * Provides prompt services. @@ -47,32 +46,6 @@ export interface IPromptPath { readonly type: PromptsType; } -/** - * Type for a shared prompt parser instance returned by the {@link IPromptsService}. - * Because the parser is shared, we omit the `dispose` method from - * the original type so the caller cannot dispose it prematurely - */ -export type TSharedPrompt = Omit; - -/** - * Metadata node object in a hierarchical tree of prompt references. - */ -export interface IMetadata { - /** - * URI of a prompt file. - */ - readonly uri: URI; - - /** - * Metadata of the prompt file. - */ - readonly metadata: TMetadata | null; - - /** - * List of metadata for each valid child prompt reference. - */ - readonly children?: readonly IMetadata[]; -} export interface ICustomChatMode { /** @@ -101,14 +74,9 @@ export interface ICustomChatMode { readonly model?: string; /** - * Contents of the custom chat mode file body. + * Contents of the custom chat mode file body and other mode instructions. */ - readonly body: string; - - /** - * References to variables without a type in the mode body. These could be tools or toolsets. - */ - readonly variableReferences: readonly IVariableReference[]; + readonly modeInstructions: IChatModeInstructions; } /** @@ -159,10 +127,10 @@ export interface IPromptsService extends IDisposable { readonly _serviceBrand: undefined; /** - * Get a prompt syntax parser for the provided text model. - * See {@link TextModelPromptParser} for more info on the parser API. + * The parsed prompt file for the provided text model. + * @param textModel Returns the parsed prompt file. */ - getSyntaxParserFor(model: ITextModel): TSharedPrompt & { isDisposed: false }; + getParsedPromptFile(textModel: ITextModel): ParsedPromptFile; /** * List all available prompt files. @@ -183,7 +151,7 @@ export interface IPromptsService extends IDisposable { /** * Gets the prompt file for a slash command. */ - resolvePromptSlashCommand(data: IChatPromptSlashCommand, _token: CancellationToken): Promise; + resolvePromptSlashCommand(data: IChatPromptSlashCommand, _token: CancellationToken): Promise; /** * Returns a prompt command if the command name is valid. @@ -204,7 +172,7 @@ export interface IPromptsService extends IDisposable { * Parses the provided URI * @param uris */ - parse(uri: URI, type: PromptsType, token: CancellationToken): Promise; + parseNew(uri: URI, token: CancellationToken): Promise; /** * Returns the prompt file type for the given URI. @@ -219,11 +187,7 @@ export interface IChatPromptSlashCommand { readonly promptPath?: IPromptPath; } - -export interface IPromptParserResult { - readonly uri: URI; - readonly metadata: TMetadata | null; - readonly topError: ITopError | undefined; - readonly fileReferences: readonly URI[]; - readonly variableReferences: readonly IVariableReference[]; +export interface IPromptHeader { + readonly node: YamlNode | undefined; + readonly errors: YamlParseError[]; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 0fa6c11c547..0bbef549c15 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -4,29 +4,30 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../../../nls.js'; -import { getLanguageIdForPromptsType, getPromptsTypeForLanguageId, MODE_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType } from '../promptTypes.js'; -import { PromptParser } from '../parsers/promptParser.js'; +import { getPromptsTypeForLanguageId, PROMPT_LANGUAGE_ID, PromptsType } from '../promptTypes.js'; import { type URI } from '../../../../../../base/common/uri.js'; -import { assert } from '../../../../../../base/common/assert.js'; import { basename } from '../../../../../../base/common/path.js'; import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { Event } from '../../../../../../base/common/event.js'; import { type ITextModel } from '../../../../../../editor/common/model.js'; -import { ObjectCache } from '../utils/objectCache.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; -import { TextModelPromptParser } from '../parsers/textModelPromptParser.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { IModelService } from '../../../../../../editor/common/services/model.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; -import type { IChatPromptSlashCommand, ICustomChatMode, IPromptParserResult, IPromptPath, IPromptsService, TPromptsStorage } from './promptsService.js'; +import type { IChatPromptSlashCommand, ICustomChatMode, IPromptPath, IPromptsService, TPromptsStorage } from './promptsService.js'; import { getCleanPromptName, PROMPT_FILE_EXTENSION } from '../config/promptFileLocations.js'; import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; import { PromptsConfig } from '../config/config.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { PositionOffsetTransformer } from '../../../../../../editor/common/core/text/positionToOffset.js'; +import { NewPromptsParser, ParsedPromptFile } from './newPromptsParser.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { ResourceMap } from '../../../../../../base/common/map.js'; +import { CancellationError } from '../../../../../../base/common/errors.js'; +import { OffsetRange } from '../../../../../../editor/common/core/ranges/offsetRange.js'; +import { IChatModeInstructions, IVariableReference } from '../../chatModes.js'; /** * Provides prompt services. @@ -34,11 +35,6 @@ import { PositionOffsetTransformer } from '../../../../../../editor/common/core/ export class PromptsService extends Disposable implements IPromptsService { public declare readonly _serviceBrand: undefined; - /** - * Cache of text model content prompt parsers. - */ - private readonly cache: ObjectCache; - /** * Prompt files locator utility. */ @@ -49,6 +45,9 @@ export class PromptsService extends Disposable implements IPromptsService { */ private cachedCustomChatModes: Promise | undefined; + + private parsedPromptFileCache = new ResourceMap<[number, ParsedPromptFile]>(); + /** * Lazily created event that is fired when the custom chat modes change. */ @@ -62,40 +61,15 @@ export class PromptsService extends Disposable implements IPromptsService { @IUserDataProfileService private readonly userDataService: IUserDataProfileService, @ILanguageService private readonly languageService: ILanguageService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IFileService private readonly fileService: IFileService, ) { super(); this.fileLocator = this._register(this.instantiationService.createInstance(PromptFilesLocator)); - // the factory function below creates a new prompt parser object - // for the provided model, if no active non-disposed parser exists - this.cache = this._register( - new ObjectCache((model) => { - assert( - model.isDisposed() === false, - 'Text model must not be disposed.', - ); - - /** - * Note! When/if shared with "file" prompts, the `seenReferences` array below must be taken into account. - * Otherwise consumers will either see incorrect failing or incorrect successful results, based on their - * use case, timing of their calls to the {@link getSyntaxParserFor} function, and state of this service. - */ - const parser: TextModelPromptParser = instantiationService.createInstance( - TextModelPromptParser, - model, - { allowNonPromptFiles: true, languageId: undefined, updateOnChange: true }, - ).start(); - - // this is a sanity check and the contract of the object cache, - // we must return a non-disposed object from this factory function - parser.assertNotDisposed( - 'Created prompt parser must not be disposed.', - ); - - return parser; - }) - ); + this._register(this.modelService.onModelRemoved((model) => { + this.parsedPromptFileCache.delete(model.uri); + })); } /** @@ -118,19 +92,16 @@ export class PromptsService extends Disposable implements IPromptsService { } - /** - * @throws {Error} if: - * - the provided model is disposed - * - newly created parser is disposed immediately on initialization. - * See factory function in the {@link constructor} for more info. - */ - public getSyntaxParserFor(model: ITextModel): TextModelPromptParser & { isDisposed: false } { - assert( - model.isDisposed() === false, - 'Cannot create a prompt syntax parser for a disposed model.', - ); - - return this.cache.get(model); + public getParsedPromptFile(textModel: ITextModel): ParsedPromptFile { + const cached = this.parsedPromptFileCache.get(textModel.uri); + if (cached && cached[0] === textModel.getVersionId()) { + return cached[1]; + } + const ast = new NewPromptsParser().parse(textModel.uri, textModel.getValue()); + if (!cached || cached[0] < textModel.getVersionId()) { + this.parsedPromptFileCache.set(textModel.uri, [textModel.getVersionId(), ast]); + } + return ast; } public async listPromptFiles(type: PromptsType, token: CancellationToken): Promise { @@ -171,13 +142,13 @@ export class PromptsService extends Disposable implements IPromptsService { return undefined; } - public async resolvePromptSlashCommand(data: IChatPromptSlashCommand, token: CancellationToken): Promise { + public async resolvePromptSlashCommand(data: IChatPromptSlashCommand, token: CancellationToken): Promise { const promptUri = await this.getPromptPath(data); if (!promptUri) { return undefined; } try { - return await this.parse(promptUri, PromptsType.prompt, token); + return await this.parseNew(promptUri, token); } catch (error) { this.logger.error(`[resolvePromptSlashCommand] Failed to parse prompt file: ${promptUri}`, error); return undefined; @@ -229,78 +200,61 @@ export class PromptsService extends Disposable implements IPromptsService { private async computeCustomChatModes(token: CancellationToken): Promise { const modeFiles = await this.listPromptFiles(PromptsType.mode, token); - const metadataList = await Promise.all( + const customChatModes = await Promise.all( modeFiles.map(async ({ uri }): Promise => { - let parser: PromptParser | undefined; - try { - // Note! this can be (and should be) improved by using shared parser instances - // that the `getSyntaxParserFor` method provides for opened documents. - parser = this.instantiationService.createInstance( - PromptParser, - uri, - { allowNonPromptFiles: true, languageId: MODE_LANGUAGE_ID, updateOnChange: false }, - ).start(token); + const ast = await this.parseNew(uri, token); - const completed = await parser.settled(); - if (!completed) { - throw new Error(localize('promptParser.notCompleted', "Prompt parser for {0} did not complete.", uri.toString())); + let metadata: any | undefined; + if (ast.header) { + const advanced = ast.header.getAttribute('advancedOptions'); + if (advanced && advanced.value.type === 'object') { + metadata = {}; + for (const [key, value] of Object.entries(advanced.value)) { + if (['string', 'number', 'boolean'].includes(value.type)) { + metadata[key] = value; + } + } } - - const body = await parser.getBody(); - const nHeaderLines = parser.header?.range.endLineNumber ?? 0; - const transformer = new PositionOffsetTransformer(body); - const variableReferences = parser.variableReferences.map(ref => { - return { - name: ref.name, - range: transformer.getOffsetRange(ref.range.delta(-nHeaderLines)) - }; - }).sort((a, b) => b.range.start - a.range.start); // in reverse order - - const name = getCleanPromptName(uri); - - const metadata = parser.metadata; - if (metadata?.promptType !== PromptsType.mode) { - return { uri, name, body, variableReferences }; - } - const { description, model, tools } = metadata; - return { uri, name, description, model, tools, body, variableReferences }; - } finally { - parser?.dispose(); } + const toolReferences: IVariableReference[] = []; + if (ast.body) { + const bodyOffset = ast.body.offset; + const bodyVarRefs = ast.body.variableReferences; + for (let i = bodyVarRefs.length - 1; i >= 0; i--) { // in reverse order + const { name, offset } = bodyVarRefs[i]; + const range = new OffsetRange(offset - bodyOffset, offset - bodyOffset + name.length + 1); + toolReferences.push({ name, range }); + } + } + + const modeInstructions = { + content: ast.body?.getContent() ?? '', + toolReferences, + metadata, + } satisfies IChatModeInstructions; + + const name = getCleanPromptName(uri); + if (!ast.header) { + return { uri, name, modeInstructions }; + } + const { description, model, tools } = ast.header; + return { uri, name, description, model, tools, modeInstructions }; + }) ); - - return metadataList; + return customChatModes; } - public async parse(uri: URI, type: PromptsType, token: CancellationToken): Promise { - let parser: PromptParser | undefined; - try { - const languageId = getLanguageIdForPromptsType(type); - parser = this.instantiationService.createInstance(PromptParser, uri, { allowNonPromptFiles: true, languageId, updateOnChange: false }).start(token); - const completed = await parser.settled(); - if (!completed) { - throw new Error(localize('promptParser.notCompleted', "Prompt parser for {0} did not complete.", uri.toString())); - } - const fullContent = await parser.getFullContent(); - const transformer = new PositionOffsetTransformer(fullContent); - const variableReferences = parser.variableReferences.map(ref => { - return { - name: ref.name, - range: transformer.getOffsetRange(ref.range) - }; - }).sort((a, b) => b.range.start - a.range.start); // in reverse order - // make a copy, to avoid leaking the parser instance - return { - uri: parser.uri, - metadata: parser.metadata, - topError: parser.topError, - variableReferences, - fileReferences: parser.references.map(ref => ref.uri) - }; - } finally { - parser?.dispose(); + public async parseNew(uri: URI, token: CancellationToken): Promise { + const model = this.modelService.getModel(uri); + if (model) { + return this.getParsedPromptFile(model); } + const fileContent = await this.fileService.readFile(uri); + if (token.isCancellationRequested) { + throw new CancellationError(); + } + return new NewPromptsParser().parse(uri, fileContent.value.toString()); } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/objectCache.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/objectCache.ts deleted file mode 100644 index 443d6e44c94..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/objectCache.ts +++ /dev/null @@ -1,153 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableMap } from '../../../../../../base/common/lifecycle.js'; -import { ObservableDisposable, assertNotDisposed } from './observableDisposable.js'; - -/** - * Generic cache for object instances. Guarantees to return only non-disposed - * objects from the {@linkcode get} method. If a requested object is not yet - * in the cache or is disposed already, the {@linkcode factory} callback is - * called to create a new object. - * - * @throws if {@linkcode factory} callback returns a disposed object. - * - * ## Examples - * - * ```typescript - * // a class that will be used as a cache key; the key can be of any - * // non-nullable type, including primitives like `string` or `number`, - * // but in this case we use an object pointer as a key - * class KeyObject {} - * - * // a class for testing purposes - * class TestObject extends ObservableDisposable { - * constructor( - * public readonly id: KeyObject, - * ) {} - * }; - * - * // create an object cache instance providing it a factory function that - * // is responsible for creating new objects based on the provided key if - * // the cache does not contain the requested object yet or an existing - * // object is already disposed - * const cache = new ObjectCache((key) => { - * // create a new test object based on the provided key - * return new TestObject(key); - * }); - * - * // create two keys - * const key1 = new KeyObject(); - * const key2 = new KeyObject(); - * - * // get an object from the cache by its key - * const object1 = cache.get(key1); // returns a new test object - * - * // validate that the new object has the correct key - * assert( - * object1.id === key1, - * 'Object 1 must have correct ID.', - * ); - * - * // returns the same cached test object - * const object2 = cache.get(key1); - * - * // validate that the same exact object is returned from the cache - * assert( - * object1 === object2, - * 'Object 2 the same cached object as object 1.', - * ); - * - * // returns a new test object - * const object3 = cache.get(key2); - * - * // validate that the new object has the correct key - * assert( - * object3.id === key2, - * 'Object 3 must have correct ID.', - * ); - * - * assert( - * object3 !== object1, - * 'Object 3 must be a new object.', - * ); - * ``` - */ -export class ObjectCache< - TValue extends ObservableDisposable, - TKey extends NonNullable = string, -> extends Disposable { - private readonly cache: DisposableMap = - this._register(new DisposableMap()); - - constructor( - private readonly factory: (key: TKey) => TValue & { isDisposed: false }, - ) { - super(); - } - - /** - * Get an existing object from the cache. If a requested object is not yet - * in the cache or is disposed already, the {@linkcode factory} callback is - * called to create a new object. - * - * @throws if {@linkcode factory} callback returns a disposed object. - * @param key - ID of the object in the cache - */ - public get(key: TKey): TValue & { isDisposed: false } { - let object = this.cache.get(key); - - // if object is already disposed, remove it from the cache - if (object?.isDisposed) { - this.cache.deleteAndLeak(key); - object = undefined; - } - - // if object exists and is not disposed, return it - if (object) { - // must always hold true due to the check above - assertNotDisposed( - object, - 'Object must not be disposed.', - ); - - return object; - } - - // create a new object by calling the factory - object = this.factory(key); - - // newly created object must not be disposed - assertNotDisposed( - object, - 'Newly created object must not be disposed.', - ); - - // remove it from the cache automatically on dispose - object.addDisposables( - object.onDispose(() => { - this.cache.deleteAndLeak(key); - })); - this.cache.set(key, object); - - return object; - } - - /** - * Remove an object from the cache by its key. - * - * @param key ID of the object to remove. - * @param dispose Whether the removed object must be disposed. - */ - public remove(key: TKey, dispose: boolean): this { - if (dispose) { - this.cache.deleteAndDispose(key); - return this; - } - - this.cache.deleteAndLeak(key); - return this; - } -} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/observableDisposable.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/observableDisposable.ts deleted file mode 100644 index a0e6a70e733..00000000000 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/observableDisposable.ts +++ /dev/null @@ -1,89 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; - -/** -* @deprecated do not use this, https://github.com/microsoft/vscode/issues/248366 - */ -export abstract class ObservableDisposable extends Disposable { - /** - * Underlying disposables store this object relies on. - */ - private readonly store = this._register(new DisposableStore()); - - /** - * Check if the current object is already has been disposed. - */ - public get isDisposed(): boolean { - return this.store.isDisposed; - } - - /** - * The event is fired when this object is disposed. - * Note! Executes the callback immediately if already disposed. - * - * @param callback The callback function to be called on updates. - */ - public onDispose(callback: () => void): IDisposable { - // if already disposed, execute the callback immediately - if (this.isDisposed) { - const timeoutHandle = setTimeout(callback); - - return toDisposable(() => { - clearTimeout(timeoutHandle); - }); - } - - return this.store.add(toDisposable(callback)); - } - - /** - * Adds disposable object(s) to the list of disposables - * that will be disposed with this object. - */ - public addDisposables(...disposables: IDisposable[]): this { - for (const disposable of disposables) { - this.store.add(disposable); - } - - return this; - } - - /** - * Assert that the current object was not yet disposed. - * - * @throws If the current object was already disposed. - * @param error Error message or error object to throw if assertion fails. - */ - public assertNotDisposed( - error: string | Error, - ): asserts this is TNotDisposed { - assertNotDisposed(this, error); - } -} - -/** - * @deprecated do not use this, https://github.com/microsoft/vscode/issues/248366 - */ -type TNotDisposed = TObject & { isDisposed: false }; - -/** - * @deprecated do not use this, https://github.com/microsoft/vscode/issues/248366 - */ -export function assertNotDisposed( - object: TObject, - error: string | Error, -): asserts object is TNotDisposed { - if (!object.isDisposed) { - return; - } - - const errorToThrow = typeof error === 'string' - ? new Error(error) - : error; - - throw errorToThrow; -} diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index 3c5bc6131e3..b580f5e4cc0 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -39,9 +39,9 @@ export interface IRawToolContribution { const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'languageModelTools', - activationEventsGenerator: (contributions: IRawToolContribution[], result) => { + activationEventsGenerator: function* (contributions: readonly IRawToolContribution[]) { for (const contrib of contributions) { - result.push(`onLanguageModelTool:${contrib.name}`); + yield `onLanguageModelTool:${contrib.name}`; } }, jsonSchema: { diff --git a/src/vs/workbench/contrib/chat/common/tools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/tools.ts index b702400ea21..e1e6d5a7b3d 100644 --- a/src/vs/workbench/contrib/chat/common/tools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/tools.ts @@ -29,7 +29,7 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo // Check if write-only mode is enabled for the todo tool const writeOnlyMode = this.configurationService.getValue(TodoListToolWriteOnlySettingId) === true; const todoToolData = createManageTodoListToolData(writeOnlyMode); - const manageTodoListTool = instantiationService.createInstance(ManageTodoListTool, writeOnlyMode); + const manageTodoListTool = this._register(instantiationService.createInstance(ManageTodoListTool, writeOnlyMode)); this._register(toolsService.registerTool(todoToolData, manageTodoListTool)); // Register the confirmation tool diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 02bb85f80de..af5d6ed6679 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -23,6 +23,8 @@ import { IChatService, IChatToolInputInvocationData } from '../../common/chatSer import { IToolData, IToolImpl, IToolInvocation, ToolDataSource } from '../../common/languageModelToolsService.js'; import { MockChatService } from '../common/mockChatService.js'; import { IConfigurationChangeEvent } from '../../../../../platform/configuration/common/configuration.js'; +import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; +import { ChatConfiguration } from '../../common/constants.js'; // --- Test helpers to reduce repetition and improve readability --- @@ -100,6 +102,7 @@ suite('LanguageModelToolsService', () => { setup(() => { configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); const instaService = workbenchInstantiationService({ contextKeyService: () => store.add(new ContextKeyService(configurationService)), configurationService: () => configurationService @@ -378,13 +381,14 @@ suite('LanguageModelToolsService', () => { }, 'Expected tool call to be cancelled'); }); - test('toToolEnablementMap', () => { + test('toToolAndToolSetEnablementMap', () => { const toolData1: IToolData = { id: 'tool1', toolReferenceName: 'refTool1', modelDescription: 'Test Tool 1', displayName: 'Test Tool 1', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; const toolData2: IToolData = { @@ -393,6 +397,7 @@ suite('LanguageModelToolsService', () => { modelDescription: 'Test Tool 2', displayName: 'Test Tool 2', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; const toolData3: IToolData = { @@ -401,6 +406,7 @@ suite('LanguageModelToolsService', () => { modelDescription: 'Test Tool 3', displayName: 'Test Tool 3', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; store.add(service.registerToolData(toolData1)); @@ -408,31 +414,66 @@ suite('LanguageModelToolsService', () => { store.add(service.registerToolData(toolData3)); // Test with enabled tools - const enabledToolNames = new Set(['refTool1']); - const result1 = service.toToolEnablementMap(enabledToolNames); + const enabledToolNames = [toolData1].map(t => service.getQualifiedToolName(t)); + const result1 = service.toToolAndToolSetEnablementMap(enabledToolNames); + + assert.strictEqual(result1.get(toolData1), true, 'tool1 should be enabled'); + assert.strictEqual(result1.get(toolData2), false, 'tool2 should be disabled'); + assert.strictEqual(result1.get(toolData3), false, 'tool3 should be disabled (no reference name)'); + + const qualifiedNames1 = service.toQualifiedToolNames(result1); + assert.deepStrictEqual(qualifiedNames1.sort(), enabledToolNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - assert.strictEqual(result1['tool1'], true, 'tool1 should be enabled'); - assert.strictEqual(result1['tool2'], false, 'tool2 should be disabled'); - assert.strictEqual(result1['tool3'], false, 'tool3 should be disabled (no reference name)'); // Test with multiple enabled tools - const multipleEnabledToolNames = new Set(['refTool1', 'refTool2']); - const result2 = service.toToolEnablementMap(multipleEnabledToolNames); + const multipleEnabledToolNames = [toolData1, toolData2].map(t => service.getQualifiedToolName(t)); + const result2 = service.toToolAndToolSetEnablementMap(multipleEnabledToolNames); + + assert.strictEqual(result1.get(toolData1), true, 'tool1 should be enabled'); + assert.strictEqual(result2.get(toolData2), true, 'tool2 should be enabled'); + assert.strictEqual(result2.get(toolData3), false, 'tool3 should be disabled'); + + const qualifiedNames2 = service.toQualifiedToolNames(result2); + assert.deepStrictEqual(qualifiedNames2.sort(), multipleEnabledToolNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - assert.strictEqual(result2['tool1'], true, 'tool1 should be enabled'); - assert.strictEqual(result2['tool2'], true, 'tool2 should be enabled'); - assert.strictEqual(result2['tool3'], false, 'tool3 should be disabled'); // Test with no enabled tools - const noEnabledToolNames = new Set(); - const result3 = service.toToolEnablementMap(noEnabledToolNames); + const noEnabledToolNames: string[] = []; + const result3 = service.toToolAndToolSetEnablementMap(noEnabledToolNames); + + assert.strictEqual(result3.get(toolData1), false, 'tool1 should be disabled'); + assert.strictEqual(result3.get(toolData2), false, 'tool2 should be disabled'); + assert.strictEqual(result3.get(toolData3), false, 'tool3 should be disabled'); + + const qualifiedNames3 = service.toQualifiedToolNames(result3); + assert.deepStrictEqual(qualifiedNames3.sort(), noEnabledToolNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - assert.strictEqual(result3['tool1'], false, 'tool1 should be disabled'); - assert.strictEqual(result3['tool2'], false, 'tool2 should be disabled'); - assert.strictEqual(result3['tool3'], false, 'tool3 should be disabled'); }); - test('toToolEnablementMap with tool sets', () => { + test('toToolAndToolSetEnablementMap with extension tool', () => { + // Register individual tools + const toolData1: IToolData = { + id: 'tool1', + toolReferenceName: 'refTool1', + modelDescription: 'Test Tool 1', + displayName: 'Test Tool 1', + source: { type: 'extension', label: "My Extension", extensionId: new ExtensionIdentifier('My.extension') }, + canBeReferencedInPrompt: true, + }; + + store.add(service.registerToolData(toolData1)); + + // Test enabling the tool set + const enabledNames = [toolData1].map(t => service.getQualifiedToolName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames); + + assert.strictEqual(result.get(toolData1), true, 'individual tool should be enabled'); + + const qualifiedNames = service.toQualifiedToolNames(result); + assert.deepStrictEqual(qualifiedNames.sort(), enabledNames.sort(), 'toQualifiedToolNames should return the original enabled names'); + }); + + test('toToolAndToolSetEnablementMap with tool sets', () => { // Register individual tools const toolData1: IToolData = { id: 'tool1', @@ -440,6 +481,7 @@ suite('LanguageModelToolsService', () => { modelDescription: 'Test Tool 1', displayName: 'Test Tool 1', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; const toolData2: IToolData = { @@ -447,6 +489,7 @@ suite('LanguageModelToolsService', () => { modelDescription: 'Test Tool 2', displayName: 'Test Tool 2', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; store.add(service.registerToolData(toolData1)); @@ -481,33 +524,52 @@ suite('LanguageModelToolsService', () => { store.add(toolSet.addTool(toolSetTool2)); // Test enabling the tool set - const enabledNames = new Set(['refToolSet', 'refTool1']); - const result = service.toToolEnablementMap(enabledNames); + const enabledNames = [toolSet, toolData1].map(t => service.getQualifiedToolName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames); - assert.strictEqual(result['tool1'], true, 'individual tool should be enabled'); - assert.strictEqual(result['tool2'], false); - assert.strictEqual(result['toolSetTool1'], true, 'tool set tool 1 should be enabled'); - assert.strictEqual(result['toolSetTool2'], true, 'tool set tool 2 should be enabled'); + assert.strictEqual(result.get(toolData1), true, 'individual tool should be enabled'); + assert.strictEqual(result.get(toolData2), false); + assert.strictEqual(result.get(toolSet), true, 'tool set should be enabled'); + assert.strictEqual(result.get(toolSetTool1), true, 'tool set tool 1 should be enabled'); + assert.strictEqual(result.get(toolSetTool2), true, 'tool set tool 2 should be enabled'); + + const qualifiedNames = service.toQualifiedToolNames(result); + assert.deepStrictEqual(qualifiedNames.sort(), enabledNames.sort(), 'toQualifiedToolNames should return the original enabled names'); }); - test('toToolEnablementMap with non-existent tool names', () => { + test('toToolAndToolSetEnablementMap with non-existent tool names', () => { const toolData: IToolData = { id: 'tool1', toolReferenceName: 'refTool1', modelDescription: 'Test Tool 1', displayName: 'Test Tool 1', source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, }; store.add(service.registerToolData(toolData)); - // Test with non-existent tool names - const enabledNames = new Set(['nonExistentTool', 'refTool1']); - const result = service.toToolEnablementMap(enabledNames); + const unregisteredToolData: IToolData = { + id: 'toolX', + toolReferenceName: 'refToolX', + modelDescription: 'Test Tool X', + displayName: 'Test Tool X', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + }; - assert.strictEqual(result['tool1'], true, 'existing tool should be enabled'); + // Test with non-existent tool names + const enabledNames = [toolData, unregisteredToolData].map(t => service.getQualifiedToolName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames); + + assert.strictEqual(result.get(toolData), true, 'existing tool should be enabled'); // Non-existent tools should not appear in the result map - assert.strictEqual(result['nonExistentTool'], undefined, 'non-existent tool should not be in result'); + assert.strictEqual(result.get(unregisteredToolData), undefined, 'non-existent tool should not be in result'); + + const qualifiedNames = service.toQualifiedToolNames(result); + const expectedNames = [service.getQualifiedToolName(toolData)]; // Only the existing tool + assert.deepStrictEqual(qualifiedNames.sort(), expectedNames.sort(), 'toQualifiedToolNames should return the original enabled names'); + }); test('accessibility signal for tool confirmation', async () => { @@ -1298,21 +1360,28 @@ suite('LanguageModelToolsService', () => { store.add(mcpToolSet.addTool(mcpTool)); // Enable the MCP toolset - const result = service.toToolAndToolSetEnablementMap(['mcpSetRef']); + { + const enabledNames = [mcpToolSet].map(t => service.getQualifiedToolName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames); - let toolSetEnabled = false; - let toolEnabled = false; - for (const [toolOrSet, enabled] of result) { - if ('referenceName' in toolOrSet && toolOrSet.referenceName === 'mcpSetRef') { - toolSetEnabled = enabled; - } - if ('id' in toolOrSet && toolOrSet.id === 'mcpTool') { - toolEnabled = enabled; - } + assert.strictEqual(result.get(mcpToolSet), true, 'MCP toolset should be enabled'); // Ensure the toolset is in the map + assert.strictEqual(result.get(mcpTool), true, 'MCP tool should be enabled when its toolset is enabled'); // Ensure the tool is in the map + + const qualifiedNames = service.toQualifiedToolNames(result); + assert.deepStrictEqual(qualifiedNames.sort(), enabledNames.sort(), 'toQualifiedToolNames should return the original enabled names'); + } + // Enable a tool from the MCP toolset + { + const enabledNames = [mcpTool].map(t => service.getQualifiedToolName(t, mcpToolSet)); + const result = service.toToolAndToolSetEnablementMap(enabledNames); + + assert.strictEqual(result.get(mcpToolSet), false, 'MCP toolset should be disabled'); // Ensure the toolset is in the map + assert.strictEqual(result.get(mcpTool), true, 'MCP tool should be enabled'); // Ensure the tool is in the map + + const qualifiedNames = service.toQualifiedToolNames(result); + assert.deepStrictEqual(qualifiedNames.sort(), enabledNames.sort(), 'toQualifiedToolNames should return the original enabled names'); } - assert.strictEqual(toolSetEnabled, true, 'MCP toolset should be enabled'); - assert.strictEqual(toolEnabled, true, 'MCP tool should be enabled when its toolset is enabled'); }); test('shouldAutoConfirm with workspace-specific tool configuration', async () => { diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts new file mode 100644 index 00000000000..b8ab36bafd6 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts @@ -0,0 +1,366 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; + +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { NewPromptsParser } from '../../../common/promptSyntax/service/newPromptsParser.js'; +import { PromptValidator } from '../../../common/promptSyntax/languageProviders/promptValidator.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { PromptsConfig } from '../../../common/promptSyntax/config/config.js'; +import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../common/languageModelToolsService.js'; +import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../common/languageModels.js'; +import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; +import { ChatMode, CustomChatMode, IChatModeService } from '../../../common/chatModes.js'; +import { MockChatModeService } from '../../common/mockChatModeService.js'; +import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; +import { IMarkerData, MarkerSeverity } from '../../../../../../platform/markers/common/markers.js'; +import { getPromptFileExtension } from '../../../common/promptSyntax/config/promptFileLocations.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { ResourceSet } from '../../../../../../base/common/map.js'; +import { ChatConfiguration } from '../../../common/constants.js'; +import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { IChatService } from '../../../common/chatService.js'; +import { MockChatService } from '../../common/mockChatService.js'; +import { LanguageModelToolsService } from '../../../browser/languageModelToolsService.js'; +import { ContextKeyService } from '../../../../../../platform/contextkey/browser/contextKeyService.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; + +suite('PromptValidator', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instaService: TestInstantiationService; + + setup(async () => { + + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(PromptsConfig.KEY, true); + testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + instaService = workbenchInstantiationService({ + contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, disposables); + const chatService = new MockChatService(); + instaService.stub(IChatService, chatService); + instaService.stub(ILabelService, { getUriLabel: (resource) => resource.path }); + + const toolService = disposables.add(instaService.createInstance(LanguageModelToolsService)); + + const testTool1 = { id: 'testTool1', displayName: 'tool1', canBeReferencedInPrompt: true, modelDescription: 'Test Tool 1', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + const testTool2 = { id: 'testTool2', displayName: 'tool2', canBeReferencedInPrompt: true, toolReferenceName: 'tool2', modelDescription: 'Test Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + const testTool3 = { id: 'testTool3', displayName: 'tool3', canBeReferencedInPrompt: true, toolReferenceName: 'tool3', modelDescription: 'Test Tool 3', source: { type: 'extension', label: "My Extension", extensionId: new ExtensionIdentifier('My.extension') }, inputSchema: {} } satisfies IToolData; + + disposables.add(toolService.registerToolData(testTool1)); + disposables.add(toolService.registerToolData(testTool2)); + disposables.add(toolService.registerToolData(testTool3)); + + instaService.set(ILanguageModelToolsService, toolService); + + const testModels: ILanguageModelChatMetadata[] = [ + { id: 'mae-4', name: 'MAE 4', vendor: 'olama', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true } } satisfies ILanguageModelChatMetadata, + { id: 'mae-4.1', name: 'MAE 4.1', vendor: 'copilot', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: true, toolCalling: true } } satisfies ILanguageModelChatMetadata, + { id: 'mae-3.5-turbo', name: 'MAE 3.5 Turbo', vendor: 'copilot', version: '1.0', family: 'mae', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024 } satisfies ILanguageModelChatMetadata + ]; + + instaService.stub(ILanguageModelsService, { + getLanguageModelIds() { return testModels.map(m => m.id); }, + lookupLanguageModel(name: string) { + return testModels.find(m => m.id === name); + } + }); + + const customChatMode = new CustomChatMode({ + uri: URI.parse('myFs://test/test/chatmode.md'), + name: 'BeastMode', + modeInstructions: { content: 'Beast mode instructions', toolReferences: [] }, + }); + instaService.stub(IChatModeService, new MockChatModeService({ builtin: [ChatMode.Agent, ChatMode.Ask, ChatMode.Edit], custom: [customChatMode] })); + + + const existingFiles = new ResourceSet([URI.parse('myFs://test/reference1.md'), URI.parse('myFs://test/reference2.md')]); + instaService.stub(IFileService, { + exists(uri: URI) { + return Promise.resolve(existingFiles.has(uri)); + } + }); + }); + + async function validate(code: string, promptType: PromptsType): Promise { + const uri = URI.parse('myFs://test/testFile' + getPromptFileExtension(promptType)); + const result = new NewPromptsParser().parse(uri, code); + const validator = instaService.createInstance(PromptValidator); + const markers: IMarkerData[] = []; + await validator.validate(result, promptType, m => markers.push(m)); + return markers; + } + suite('modes', () => { + + test('correct mode', async () => { + const content = [ + /* 01 */"---", + /* 02 */`description: "Agent mode test"`, + /* 03 */"model: MAE 4.1", + /* 04 */"tools: ['tool1', 'tool2']", + /* 05 */"---", + /* 06 */"This is a chat mode test.", + /* 07 */"Here is a #tool1 variable and a #file:./reference1.md as well as a [reference](./reference2.md).", + ].join('\n'); + const markers = await validate(content, PromptsType.mode); + assert.deepStrictEqual(markers, []); + }); + + test('mode with errors (empty description, unknown tool & model)', async () => { + const content = [ + /* 01 */"---", + /* 02 */`description: ""`, // empty description -> error + /* 03 */"model: MAE 4.2", // unknown model -> warning + /* 04 */"tools: ['tool1', 'tool2', 'tool4', 'my.extension/tool3']", // tool4 unknown -> error + /* 05 */"---", + /* 06 */"Body", + ].join('\n'); + const markers = await validate(content, PromptsType.mode); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: "The 'description' attribute should not be empty." }, + { severity: MarkerSeverity.Error, message: "Unknown tool 'tool4'." }, + { severity: MarkerSeverity.Warning, message: "Unknown model 'MAE 4.2'." }, + ] + ); + }); + + test('tools must be array', async () => { + const content = [ + "---", + "description: \"Test\"", + "tools: 'tool1'", + "---", + ].join('\n'); + const markers = await validate(content, PromptsType.mode); + assert.strictEqual(markers.length, 1); + assert.deepStrictEqual(markers.map(m => m.message), ["The 'tools' attribute must be an array."]); + }); + + test('each tool must be string', async () => { + const content = [ + "---", + "description: \"Test\"", + "tools: ['tool1', 2]", + "---", + ].join('\n'); + const markers = await validate(content, PromptsType.mode); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: "Each tool name in the 'tools' attribute must be a string." }, + ] + ); + }); + + test('old tool reference', async () => { + const content = [ + "---", + "description: \"Test\"", + "tools: ['tool1', 'tool3']", + "---", + ].join('\n'); + const markers = await validate(content, PromptsType.mode); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Warning, message: "Tool or toolset 'tool3' is deprecated, use 'my.extension/tool3' instead." }, + ] + ); + }); + + test('unknown attribute in mode file', async () => { + const content = [ + "---", + "description: \"Test\"", + "applyTo: '*.ts'", // not allowed in mode file + "---", + ].join('\n'); + const markers = await validate(content, PromptsType.mode); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.ok(markers[0].message.startsWith("Attribute 'applyTo' is not supported in mode files.")); + }); + }); + + suite('instructions', () => { + + test('instructions valid', async () => { + const content = [ + "---", + "description: \"Instr\"", + "applyTo: *.ts,*.js", + "---", + ].join('\n'); + const markers = await validate(content, PromptsType.instructions); + assert.deepEqual(markers, []); + }); + + test('instructions invalid applyTo type', async () => { + const content = [ + "---", + "description: \"Instr\"", + "applyTo: 5", + "---", + ].join('\n'); + const markers = await validate(content, PromptsType.instructions); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].message, "The 'applyTo' attribute must be a string."); + }); + + test('instructions invalid applyTo glob & unknown attribute', async () => { + const content = [ + "---", + "description: \"Instr\"", + "applyTo: ''", // empty -> invalid glob + "model: mae-4", // model not allowed in instructions + "---", + ].join('\n'); + const markers = await validate(content, PromptsType.instructions); + assert.strictEqual(markers.length, 2); + // Order: unknown attribute warnings first (attribute iteration) then applyTo validation + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.ok(markers[0].message.startsWith("Attribute 'model' is not supported in instructions files.")); + assert.strictEqual(markers[1].message, "The 'applyTo' attribute must be a valid glob pattern."); + }); + + test('invalid header structure (YAML array)', async () => { + const content = [ + "---", + "- item1", + "---", + "Body", + ].join('\n'); + const markers = await validate(content, PromptsType.instructions); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].message, 'Invalid header, expecting pairs'); + }); + }); + + suite('prompts', () => { + + test('prompt valid with agent mode (default) and tools and a BYO model', async () => { + // mode omitted -> defaults to Agent; tools+model should validate; model MAE 4 is agent capable + const content = [ + '---', + 'description: "Prompt with tools"', + "model: MAE 4.1", + "tools: ['tool1','tool2']", + '---', + 'Body' + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.deepStrictEqual(markers, []); + }); + + test('prompt model not suited for agent mode', async () => { + // MAE 3.5 Turbo lacks agentMode capability -> warning when used in agent (default) mode + const content = [ + '---', + 'description: "Prompt with unsuitable model"', + "model: MAE 3.5 Turbo", + '---', + 'Body' + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.strictEqual(markers.length, 1, 'Expected one warning about unsuitable model'); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.strictEqual(markers[0].message, "Model 'MAE 3.5 Turbo' is not suited for agent mode."); + }); + + test('prompt with custom mode BeastMode and tools', async () => { + // Explicit custom mode should be recognized; BeastMode kind comes from setup; ensure tools accepted + const content = [ + '---', + 'description: "Prompt custom mode"', + 'mode: BeastMode', + "tools: ['tool1']", + '---', + 'Body' + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.deepStrictEqual(markers, []); + }); + + test('prompt with unknown mode Ask', async () => { + const content = [ + '---', + 'description: "Prompt unknown mode Ask"', + 'mode: Ask', + "tools: ['tool1','tool2']", + '---', + 'Body' + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.strictEqual(markers.length, 1, 'Expected one warning about tools in non-agent mode'); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.strictEqual(markers[0].message, "Unknown mode 'Ask'. Available modes: agent, ask, edit, BeastMode."); + }); + + test('prompt with mode edit', async () => { + const content = [ + '---', + 'description: "Prompt edit mode with tool"', + 'mode: edit', + "tools: ['tool1']", + '---', + 'Body' + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.strictEqual(markers[0].message, "The 'tools' attribute is only supported in agent mode. Attribute will be ignored."); + }); + }); + + suite('body', () => { + test('body with existing file references and known tools has no markers', async () => { + const content = [ + '---', + 'description: "Refs"', + '---', + 'Here is a #file:./reference1.md and a markdown [reference](./reference2.md) plus variables #tool1 and #tool2' + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.deepStrictEqual(markers, [], 'Expected no validation issues'); + }); + + test('body with missing file references reports warnings', async () => { + const content = [ + '---', + 'description: "Missing Refs"', + '---', + 'Here is a #file:./missing1.md and a markdown [missing link](./missing2.md).' + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + const messages = markers.map(m => m.message).sort(); + assert.deepStrictEqual(messages, [ + "File './missing1.md' not found at '/missing1.md'.", + "File './missing2.md' not found at '/missing2.md'." + ]); + }); + + test('body with unknown tool variable reference warns', async () => { + const content = [ + '---', + 'description: "Unknown tool var"', + '---', + 'This line references known #tool1 and unknown #toolX' + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.strictEqual(markers.length, 1, 'Expected one warning for unknown tool variable'); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, "Unknown tool or toolset 'toolX'."); + }); + + }); + +}); diff --git a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts index 09d2629d456..9b17fbdd988 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts @@ -110,8 +110,8 @@ suite('ChatModeService', () => { name: 'Test Mode', description: 'A test custom mode', tools: ['tool1', 'tool2'], - body: 'Custom mode body', - variableReferences: [] + modeInstructions: { content: 'Custom mode body', toolReferences: [] } + }; promptsService.setCustomModes([customMode]); @@ -129,7 +129,7 @@ suite('ChatModeService', () => { assert.strictEqual(testMode.description.get(), customMode.description); assert.strictEqual(testMode.kind, ChatModeKind.Agent); assert.deepStrictEqual(testMode.customTools?.get(), customMode.tools); - assert.strictEqual(testMode.body?.get(), customMode.body); + assert.deepStrictEqual(testMode.modeInstructions?.get(), customMode.modeInstructions); assert.strictEqual(testMode.uri?.get().toString(), customMode.uri.toString()); }); @@ -144,8 +144,7 @@ suite('ChatModeService', () => { name: 'Test Mode', description: 'A test custom mode', tools: [], - body: 'Custom mode body', - variableReferences: [] + modeInstructions: { content: 'Custom mode body', toolReferences: [] }, }; promptsService.setCustomModes([customMode]); @@ -162,8 +161,7 @@ suite('ChatModeService', () => { name: 'Findable Mode', description: 'A findable custom mode', tools: [], - body: 'Findable mode body', - variableReferences: [] + modeInstructions: { content: 'Findable mode body', toolReferences: [] }, }; promptsService.setCustomModes([customMode]); @@ -185,9 +183,8 @@ suite('ChatModeService', () => { name: 'Initial Mode', description: 'Initial description', tools: ['tool1'], - body: 'Initial body', + modeInstructions: { content: 'Initial body', toolReferences: [] }, model: 'gpt-4', - variableReferences: [] }; promptsService.setCustomModes([initialMode]); @@ -202,7 +199,7 @@ suite('ChatModeService', () => { ...initialMode, description: 'Updated description', tools: ['tool1', 'tool2'], - body: 'Updated body', + modeInstructions: { content: 'Updated body', toolReferences: [] }, model: 'Updated model' }; @@ -218,7 +215,7 @@ suite('ChatModeService', () => { // But the observable properties should be updated assert.strictEqual(updatedCustomMode.description.get(), 'Updated description'); assert.deepStrictEqual(updatedCustomMode.customTools?.get(), ['tool1', 'tool2']); - assert.strictEqual(updatedCustomMode.body?.get(), 'Updated body'); + assert.deepStrictEqual(updatedCustomMode.modeInstructions?.get(), { content: 'Updated body', toolReferences: [] }); assert.strictEqual(updatedCustomMode.model?.get(), 'Updated model'); }); @@ -228,8 +225,7 @@ suite('ChatModeService', () => { name: 'Mode 1', description: 'First mode', tools: [], - body: 'Mode 1 body', - variableReferences: [] + modeInstructions: { content: 'Mode 1 body', toolReferences: [] }, }; const mode2: ICustomChatMode = { @@ -237,8 +233,7 @@ suite('ChatModeService', () => { name: 'Mode 2', description: 'Second mode', tools: [], - body: 'Mode 2 body', - variableReferences: [] + modeInstructions: { content: 'Mode 2 body', toolReferences: [] }, }; // Add both modes diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatModeService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatModeService.ts index 300b1f202fc..fc3cdb78daf 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatModeService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatModeService.ts @@ -10,10 +10,10 @@ import { ChatMode, IChatMode, IChatModeService } from '../../common/chatModes.js export class MockChatModeService implements IChatModeService { readonly _serviceBrand: undefined; - private _modes: { builtin: readonly IChatMode[]; custom: readonly IChatMode[] } = { builtin: [ChatMode.Ask], custom: [] }; - public readonly onDidChangeChatModes = Event.None; + constructor(private readonly _modes: { builtin: readonly IChatMode[]; custom: readonly IChatMode[] } = { builtin: [ChatMode.Ask], custom: [] }) { } + getModes(): { builtin: readonly IChatMode[]; custom: readonly IChatMode[] } { return this._modes; } diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index dd2c2aa4085..da9afffb06c 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -13,9 +13,13 @@ import { IChatCompleteResponse, IChatDetail, IChatProviderInfo, IChatSendRequest import { ChatAgentLocation } from '../../common/constants.js'; export class MockChatService implements IChatService { + getChatSessionFromInternalId(modelSessionId: string): { chatSessionType: string; chatSessionId: string; isUntitled: boolean } | undefined { + throw new Error('Method not implemented.'); + } requestInProgressObs = observableValue('name', false); edits2Enabled: boolean = false; _serviceBrand: undefined; + editingSessions = []; transferredSessionData: IChatTransferredSessionData | undefined; onDidSubmitRequest: Event<{ chatSessionId: string }> = Event.None; diff --git a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts index 2d33f4a1169..c795fc769c9 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts @@ -82,10 +82,6 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService throw new Error('Method not implemented.'); } - toToolEnablementMap(toolOrToolSetNames: Set): Record { - throw new Error('Method not implemented.'); - } - toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap { throw new Error('Method not implemented.'); } @@ -93,4 +89,24 @@ export class MockLanguageModelToolsService implements ILanguageModelToolsService toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] { throw new Error('Method not implemented.'); } + + getQualifiedToolNames(): Iterable { + throw new Error('Method not implemented.'); + } + + getToolByQualifiedName(qualifiedName: string): IToolData | ToolSet | undefined { + throw new Error('Method not implemented.'); + } + + getQualifiedToolName(tool: IToolData, set?: ToolSet): string { + throw new Error('Method not implemented.'); + } + + toQualifiedToolNames(map: IToolAndToolSetEnablementMap): string[] { + throw new Error('Method not implemented.'); + } + + getDeprecatedQualifiedToolNames(): Map { + throw new Error('Method not implemented.'); + } } diff --git a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts index 97394b57d90..93709eefffc 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts @@ -6,6 +6,8 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Emitter } from '../../../../../base/common/event.js'; import { URI } from '../../../../../base/common/uri.js'; +import { ITextModel } from '../../../../../editor/common/model.js'; +import { ParsedPromptFile } from '../../common/promptSyntax/service/newPromptsParser.js'; import { ICustomChatMode, IPromptsService } from '../../common/promptSyntax/service/promptsService.js'; export class MockPromptsService implements IPromptsService { @@ -33,6 +35,8 @@ export class MockPromptsService implements IPromptsService { resolvePromptSlashCommand(_data: any, _token: CancellationToken): Promise { throw new Error('Not implemented'); } findPromptSlashCommands(): Promise { throw new Error('Not implemented'); } parse(_uri: URI, _type: any, _token: CancellationToken): Promise { throw new Error('Not implemented'); } + parseNew(_uri: URI, _token: CancellationToken): Promise { throw new Error('Not implemented'); } getPromptFileType(_resource: URI): any { return undefined; } + getParsedPromptFile(textModel: ITextModel): ParsedPromptFile { throw new Error('Not implemented'); } dispose(): void { } } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterBoolean.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterBoolean.test.ts deleted file mode 100644 index 8d32c30d851..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterBoolean.test.ts +++ /dev/null @@ -1,317 +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 assert from 'assert'; -import { Range } from '../../../../../../../../../editor/common/core/range.js'; -import { Word } from '../../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/tokens.js'; -import { randomBoolean } from '../../../../../../../../../base/test/common/testUtils.js'; -import { FrontMatterBoolean } from '../../../../../../common/promptSyntax/codecs/base/frontMatterCodec/tokens/index.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../../../base/test/common/utils.js'; -import { FrontMatterSequence } from '../../../../../../common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterSequence.js'; - -suite('FrontMatterBoolean', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - suite('equals()', () => { - suite('base case', () => { - test('true', () => { - // both values should yield the same result - const booleanText = (randomBoolean()) - ? 'true' - : 'TRUE'; - - const boolean = new FrontMatterBoolean( - new Word( - new Range(1, 1, 1, 5), - booleanText, - ), - ); - - const other = new FrontMatterBoolean( - new Word( - new Range(1, 1, 1, 5), - booleanText, - ), - ); - - assert.strictEqual( - boolean.value, - true, - 'Must have correct boolean value.', - ); - - assert( - boolean.equals(other), - 'Booleans must be equal.', - ); - }); - - test('false', () => { - // both values should yield the same result - const booleanText = (randomBoolean()) - ? 'false' - : 'FALSE'; - - const boolean = new FrontMatterBoolean( - new Word( - new Range(5, 15, 5, 15 + 6), - booleanText, - ), - ); - - const other = new FrontMatterBoolean( - new Word( - new Range(5, 15, 5, 15 + 6), - booleanText, - ), - ); - - assert.strictEqual( - boolean.value, - false, - 'Must have correct boolean value.', - ); - - assert( - boolean.equals(other), - 'Booleans must be equal.', - ); - }); - }); - - suite('non-boolean token', () => { - suite('word token', () => { - test('true', () => { - // both values should yield the same result - const booleanText = (randomBoolean()) - ? 'true' - : 'TRUE'; - - const boolean = new FrontMatterBoolean( - new Word( - new Range(1, 1, 1, 5), - booleanText, - ), - ); - - const other = new Word( - new Range(1, 1, 1, 5), - booleanText, - ); - - assert( - boolean.equals(other) === false, - 'Booleans must not be equal.', - ); - }); - - test('false', () => { - // both values should yield the same result - const booleanText = (randomBoolean()) - ? 'false' - : 'FALSE'; - - const boolean = new FrontMatterBoolean( - new Word( - new Range(1, 2, 1, 2 + 6), - booleanText, - ), - ); - - const other = new Word( - new Range(1, 2, 1, 2 + 6), - booleanText, - ); - - assert( - boolean.equals(other) === false, - 'Booleans must not be equal.', - ); - }); - }); - - suite('sequence token', () => { - test('true', () => { - // both values should yield the same result - const booleanText = (randomBoolean()) - ? 'true' - : 'TRUE'; - - const boolean = new FrontMatterBoolean( - new Word( - new Range(1, 1, 1, 5), - booleanText, - ), - ); - - const other = new FrontMatterSequence([ - new Word( - new Range(1, 1, 1, 5), - booleanText, - ), - ]); - - assert( - boolean.equals(other) === false, - 'Booleans must not be equal.', - ); - }); - - test('false', () => { - // both values should yield the same result - const booleanText = (randomBoolean()) - ? 'false' - : 'FALSE'; - - const boolean = new FrontMatterBoolean( - new Word( - new Range(1, 2, 1, 2 + 6), - booleanText, - ), - ); - - const other = new FrontMatterSequence([ - new Word( - new Range(1, 2, 1, 2 + 6), - booleanText, - ), - ]); - - assert( - boolean.equals(other) === false, - 'Booleans must not be equal.', - ); - }); - }); - }); - - suite('different range', () => { - test('true', () => { - // both values should yield the same result - const booleanText = (randomBoolean()) - ? 'true' - : 'TRUE'; - - const boolean = new FrontMatterBoolean( - new Word( - new Range(1, 2, 1, 2 + 4), - booleanText, - ), - ); - - const other = new FrontMatterBoolean( - new Word( - new Range(3, 2, 3, 2 + 4), - booleanText, - ), - ); - - assert( - boolean.equals(other) === false, - 'Booleans must not be equal.', - ); - }); - - test('false', () => { - // both values should yield the same result - const booleanText = (randomBoolean()) - ? 'false' - : 'FALSE'; - - const boolean = new FrontMatterBoolean( - new Word( - new Range(5, 15, 5, 15 + 5), - booleanText, - ), - ); - - const other = new FrontMatterBoolean( - new Word( - new Range(4, 15, 4, 15 + 5), - booleanText, - ), - ); - - assert( - boolean.equals(other) === false, - 'Booleans must not be equal.', - ); - }); - }); - - suite('different text', () => { - test('true', () => { - const boolean = new FrontMatterBoolean( - new Word( - new Range(1, 1, 1, 5), - 'true', - ), - ); - - const other = new FrontMatterBoolean( - new Word( - new Range(1, 1, 1, 5), - 'True', - ), - ); - - assert( - boolean.equals(other) === false, - 'Booleans must not be equal.', - ); - }); - - test('false', () => { - const boolean = new FrontMatterBoolean( - new Word( - new Range(5, 15, 5, 15 + 6), - 'FALSE', - ), - ); - - const other = new FrontMatterBoolean( - new Word( - new Range(5, 15, 5, 15 + 6), - 'false', - ), - ); - - assert( - boolean.equals(other) === false, - 'Booleans must not be equal.', - ); - }); - }); - - test('throws if cannot be converted to a boolean', () => { - assert.throws(() => { - new FrontMatterBoolean( - new Word( - new Range(1, 1, 1, 5), - 'true1', - ), - ); - }); - - assert.throws(() => { - new FrontMatterBoolean( - new Word( - new Range(2, 5, 2, 5 + 6), - 'fal se', - ), - ); - }); - - assert.throws(() => { - new FrontMatterBoolean( - new Word( - new Range(20, 4, 20, 4 + 1), - '1', - ), - ); - }); - }); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterDecoder.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterDecoder.test.ts deleted file mode 100644 index b4f78b9d19b..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterDecoder.test.ts +++ /dev/null @@ -1,415 +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 { Range } from '../../../../../../../../../editor/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/promptSyntax/codecs/base/linesCodec/tokens/newLine.js'; -import { DoubleQuote } from '../../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/doubleQuote.js'; -import { type TSimpleDecoderToken } from '../../../../../../common/promptSyntax/codecs/base/simpleCodec/simpleDecoder.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../../../base/test/common/utils.js'; -import { LeftBracket, RightBracket } from '../../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/brackets.js'; -import { FrontMatterDecoder } from '../../../../../../common/promptSyntax/codecs/base/frontMatterCodec/frontMatterDecoder.js'; -import { FrontMatterSequence } from '../../../../../../common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterSequence.js'; -import { ExclamationMark, Quote, Tab, Word, Space, Colon, VerticalTab, Comma, Dash } from '../../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/tokens.js'; -import { FrontMatterBoolean, FrontMatterString, FrontMatterArray, FrontMatterRecord, FrontMatterRecordDelimiter, FrontMatterRecordName } from '../../../../../../common/promptSyntax/codecs/base/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 + 5), 'write'), - new Dash(new Range(2, 6, 2, 7)), - new Word(new Range(2, 7, 2, 7 + 4), '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 Word(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 Word(new Range(3, 22, 3, 22 + 5), 'FALSE'), - ), - ]), - new Space(new Range(3, 27, 3, 28)), - ]); - }); - - suite('record', () => { - suite('values', () => { - test('unquoted string', async () => { - const test = disposables.add(new TestFrontMatterDecoder()); - - await test.run( - [ - 'just: write some yaml ', - 'anotherField \t\t : fal\v \t', - ], - [ - // 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 FrontMatterSequence([ - new Word(new Range(1, 7, 1, 7 + 5), 'write'), - new Space(new Range(1, 12, 1, 13)), - new Word(new Range(1, 13, 1, 13 + 4), 'some'), - new Space(new Range(1, 17, 1, 18)), - new Word(new Range(1, 18, 1, 18 + 4), 'yaml'), - ]), - ]), - new Space(new Range(1, 22, 1, 23)), - new NewLine(new Range(1, 23, 1, 24)), - // second record - new FrontMatterRecord([ - new FrontMatterRecordName([ - new Word(new Range(2, 1, 2, 1 + 12), 'anotherField'), - ]), - new FrontMatterRecordDelimiter([ - new Colon(new Range(2, 17, 2, 18)), - new Space(new Range(2, 18, 2, 19)), - ]), - new FrontMatterSequence([ - new Word(new Range(2, 20, 2, 20 + 3), 'fal'), - ]), - ]), - new VerticalTab(new Range(2, 23, 2, 24)), - new Space(new Range(2, 24, 2, 25)), - new Tab(new Range(2, 25, 2, 26)), - ]); - }); - - test('quoted string', async () => { - const test = disposables.add(new TestFrontMatterDecoder()); - - await test.run( - [ - `just\t:\t'\vdo\tsome\ntesting, please\v' `, - 'anotherField \t\t :\v\v"fal\nse"', - ], - [ - // first record - new FrontMatterRecord([ - new FrontMatterRecordName([ - new Word(new Range(1, 1, 1, 1 + 4), 'just'), - ]), - new FrontMatterRecordDelimiter([ - new Colon(new Range(1, 6, 1, 7)), - new Tab(new Range(1, 7, 1, 8)), - ]), - new FrontMatterString([ - new Quote(new Range(1, 8, 1, 9)), - new VerticalTab(new Range(1, 9, 1, 10)), - new Word(new Range(1, 10, 1, 10 + 2), 'do'), - new Tab(new Range(1, 12, 1, 13)), - new Word(new Range(1, 13, 1, 13 + 4), 'some'), - new NewLine(new Range(1, 17, 1, 18)), - new Word(new Range(2, 1, 2, 1 + 7), 'testing'), - new Comma(new Range(2, 8, 2, 9)), - new Space(new Range(2, 9, 2, 10)), - new Word(new Range(2, 10, 2, 10 + 6), 'please'), - new VerticalTab(new Range(2, 16, 2, 17)), - new Quote(new Range(2, 17, 2, 18)), - ]), - ]), - new Space(new Range(2, 18, 2, 19)), - new NewLine(new Range(2, 19, 2, 20)), - // second record - new FrontMatterRecord([ - new FrontMatterRecordName([ - new Word(new Range(3, 1, 3, 1 + 12), 'anotherField'), - ]), - new FrontMatterRecordDelimiter([ - new Colon(new Range(3, 17, 3, 18)), - new VerticalTab(new Range(3, 18, 3, 19)), - ]), - new FrontMatterString([ - new DoubleQuote(new Range(3, 20, 3, 21)), - new Word(new Range(3, 21, 3, 21 + 3), 'fal'), - new NewLine(new Range(3, 24, 3, 25)), - new Word(new Range(4, 1, 4, 1 + 2), 'se'), - new DoubleQuote(new Range(4, 3, 4, 4)), - ]), - ]), - ]); - }); - - test('boolean', async () => { - const test = disposables.add(new TestFrontMatterDecoder()); - - await test.run( - [ - 'anotherField \t\t : FALSE ', - 'my-field: true\t ', - ], - [ - // first record - new FrontMatterRecord([ - new FrontMatterRecordName([ - new Word(new Range(1, 1, 1, 1 + 12), 'anotherField'), - ]), - new FrontMatterRecordDelimiter([ - new Colon(new Range(1, 17, 1, 18)), - new Space(new Range(1, 18, 1, 19)), - ]), - new FrontMatterBoolean( - new Word( - new Range(1, 20, 1, 20 + 5), - 'FALSE', - ), - ), - ]), - new Space(new Range(1, 25, 1, 26)), - new NewLine(new Range(1, 26, 1, 27)), - // second record - new FrontMatterRecord([ - new FrontMatterRecordName([ - new Word(new Range(2, 1, 2, 1 + 2), 'my'), - new Dash(new Range(2, 3, 2, 4)), - new Word(new Range(2, 4, 2, 4 + 5), 'field'), - ]), - new FrontMatterRecordDelimiter([ - new Colon(new Range(2, 9, 2, 10)), - new Space(new Range(2, 10, 2, 11)), - ]), - new FrontMatterBoolean( - new Word( - new Range(2, 11, 2, 11 + 4), - 'true', - ), - ), - ]), - new Tab(new Range(2, 15, 2, 16)), - new Space(new Range(2, 16, 2, 17)), - ]); - }); - - suite('array', () => { - test('empty', async () => { - const test = disposables.add(new TestFrontMatterDecoder()); - - await test.run( - [ - `tools\v:\t []`, - 'anotherField \t\t :\v\v"fal\nse"', - ], - [ - // first record - new FrontMatterRecord([ - new FrontMatterRecordName([ - new Word(new Range(1, 1, 1, 1 + 5), 'tools'), - ]), - new FrontMatterRecordDelimiter([ - new Colon(new Range(1, 7, 1, 8)), - new Tab(new Range(1, 8, 1, 9)), - ]), - new FrontMatterArray([ - new LeftBracket(new Range(1, 10, 1, 11)), - new RightBracket(new Range(1, 11, 1, 12)), - ]), - ]), - new NewLine(new Range(1, 12, 1, 13)), - // second record - new FrontMatterRecord([ - new FrontMatterRecordName([ - new Word(new Range(2, 1, 2, 1 + 12), 'anotherField'), - ]), - new FrontMatterRecordDelimiter([ - new Colon(new Range(2, 17, 2, 18)), - new VerticalTab(new Range(2, 18, 2, 19)), - ]), - new FrontMatterString([ - new DoubleQuote(new Range(2, 20, 2, 21)), - new Word(new Range(2, 21, 2, 21 + 3), 'fal'), - new NewLine(new Range(2, 24, 2, 25)), - new Word(new Range(3, 1, 3, 1 + 2), 'se'), - new DoubleQuote(new Range(3, 3, 3, 4)), - ]), - ]), - ]); - }); - - test('mixed values', async () => { - const test = disposables.add(new TestFrontMatterDecoder()); - - await test.run( - [ - `tools\v:\t [true , 'toolName', some-tool]`, - ], - [ - // first record - new FrontMatterRecord([ - new FrontMatterRecordName([ - new Word(new Range(1, 1, 1, 1 + 5), 'tools'), - ]), - new FrontMatterRecordDelimiter([ - new Colon(new Range(1, 7, 1, 8)), - new Tab(new Range(1, 8, 1, 9)), - ]), - new FrontMatterArray([ - new LeftBracket(new Range(1, 10, 1, 11)), - // first array value - new FrontMatterBoolean( - new Word( - new Range(1, 11, 1, 11 + 4), - 'true', - ), - ), - // second array value - new FrontMatterString([ - new Quote(new Range(1, 18, 1, 19)), - new Word(new Range(1, 19, 1, 19 + 8), 'toolName'), - new Quote(new Range(1, 27, 1, 28)), - ]), - // third array value - new FrontMatterSequence([ - new Word(new Range(1, 30, 1, 30 + 4), 'some'), - new Dash(new Range(1, 34, 1, 35)), - new Word(new Range(1, 35, 1, 35 + 4), 'tool'), - ]), - new RightBracket(new Range(1, 39, 1, 40)), - ]), - ]), - ]); - }); - - test('redundant commas', async () => { - const test = disposables.add(new TestFrontMatterDecoder()); - - await test.run( - [ - `tools\v:\t [true ,, 'toolName', , , some-tool ,]`, - ], - [ - // first record - new FrontMatterRecord([ - new FrontMatterRecordName([ - new Word(new Range(1, 1, 1, 1 + 5), 'tools'), - ]), - new FrontMatterRecordDelimiter([ - new Colon(new Range(1, 7, 1, 8)), - new Tab(new Range(1, 8, 1, 9)), - ]), - new FrontMatterArray([ - new LeftBracket(new Range(1, 10, 1, 11)), - // first array value - new FrontMatterBoolean( - new Word( - new Range(1, 11, 1, 11 + 4), - 'true', - ), - ), - // second array value - new FrontMatterString([ - new Quote(new Range(1, 19, 1, 20)), - new Word(new Range(1, 20, 1, 20 + 8), 'toolName'), - new Quote(new Range(1, 28, 1, 29)), - ]), - // third array value - new FrontMatterSequence([ - new Word(new Range(1, 35, 1, 35 + 4), 'some'), - new Dash(new Range(1, 39, 1, 40)), - new Word(new Range(1, 40, 1, 40 + 4), 'tool'), - ]), - new RightBracket(new Range(1, 47, 1, 48)), - ]), - ]), - ]); - }); - }); - }); - }); - - test('empty', async () => { - const test = disposables.add( - new TestFrontMatterDecoder(), - ); - - await test.run('', []); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterRecord.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterRecord.test.ts deleted file mode 100644 index fba57e45209..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterRecord.test.ts +++ /dev/null @@ -1,183 +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 assert from 'assert'; -import { Range } from '../../../../../../../../../editor/common/core/range.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../../../base/test/common/utils.js'; -import { FrontMatterSequence } from '../../../../../../common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterSequence.js'; -import { Colon, LeftBracket, Quote, RightBracket, Space, Tab, VerticalTab, Word } from '../../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/tokens.js'; -import { FrontMatterArray, FrontMatterBoolean, FrontMatterRecord, FrontMatterRecordDelimiter, FrontMatterRecordName, FrontMatterString } from '../../../../../../common/promptSyntax/codecs/base/frontMatterCodec/tokens/index.js'; - -suite('FrontMatterBoolean', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - suite('trimValueEnd()', () => { - test('trims space tokens at the end of record\'s value', () => { - const recordName = new FrontMatterRecordName([ - new Word( - new Range(4, 10, 4, 10 + 3), - 'key', - ), - ]); - - const recordDelimiter = new FrontMatterRecordDelimiter([ - new Colon(new Range(4, 14, 4, 15)), - new VerticalTab(new Range(4, 15, 4, 16)), - ]); - - const recordValue = new FrontMatterSequence([ - new Word(new Range(4, 18, 4, 18 + 10), 'some-value'), - new VerticalTab(new Range(4, 28, 4, 29)), - new Tab(new Range(4, 29, 4, 30)), - new Space(new Range(4, 30, 4, 31)), - new Tab(new Range(4, 31, 4, 32)), - ]); - - const record = new FrontMatterRecord([ - recordName, recordDelimiter, recordValue, - ]); - - const trimmed = record.trimValueEnd(); - assert.deepStrictEqual( - trimmed, - [ - new VerticalTab(new Range(4, 28, 4, 29)), - new Tab(new Range(4, 29, 4, 30)), - new Space(new Range(4, 30, 4, 31)), - new Tab(new Range(4, 31, 4, 32)), - ], - 'Must return correct trimmed list of spacing tokens.', - ); - - assert( - record.range.equalsRange( - new Range(4, 10, 4, 28), - ), - 'Must correctly update token range.', - ); - }); - - suite('does not trim non-sequence value tokens', () => { - test('boolean', () => { - const recordName = new FrontMatterRecordName([ - new Word( - new Range(4, 10, 4, 10 + 3), - 'yke', - ), - ]); - - const recordDelimiter = new FrontMatterRecordDelimiter([ - new Colon(new Range(4, 14, 4, 15)), - new VerticalTab(new Range(4, 15, 4, 16)), - ]); - - const recordValue = new FrontMatterBoolean( - new Word(new Range(4, 18, 4, 18 + 4), 'true'), - ); - - const record = new FrontMatterRecord([ - recordName, recordDelimiter, recordValue, - ]); - - const trimmed = record.trimValueEnd(); - assert.deepStrictEqual( - trimmed, - [], - 'Must return empty list of trimmed spacing tokens.', - ); - - assert( - record.range.equalsRange( - new Range(4, 10, 4, 22), - ), - 'Must not update token range.', - ); - }); - - test('quoted string', () => { - const recordName = new FrontMatterRecordName([ - new Word( - new Range(4, 10, 4, 10 + 3), - 'eyk', - ), - ]); - - const recordDelimiter = new FrontMatterRecordDelimiter([ - new Colon(new Range(4, 14, 4, 15)), - new VerticalTab(new Range(4, 15, 4, 16)), - ]); - - const recordValue = new FrontMatterString([ - new Quote(new Range(4, 18, 4, 19)), - new Word(new Range(4, 19, 4, 19 + 10), 'some text'), - new Quote(new Range(4, 29, 4, 30)), - ]); - - const record = new FrontMatterRecord([ - recordName, recordDelimiter, recordValue, - ]); - - const trimmed = record.trimValueEnd(); - assert.deepStrictEqual( - trimmed, - [], - 'Must return empty list of trimmed spacing tokens.', - ); - - assert( - record.range.equalsRange( - new Range(4, 10, 4, 30), - ), - 'Must not update token range.', - ); - }); - - test('array', () => { - const recordName = new FrontMatterRecordName([ - new Word( - new Range(4, 10, 4, 10 + 3), - 'yek', - ), - ]); - - const recordDelimiter = new FrontMatterRecordDelimiter([ - new Colon(new Range(4, 14, 4, 15)), - new VerticalTab(new Range(4, 15, 4, 16)), - ]); - - const recordValue = new FrontMatterArray([ - new LeftBracket(new Range(4, 18, 4, 19)), - new FrontMatterString([ - new Quote(new Range(4, 18, 4, 19)), - new Word(new Range(4, 19, 4, 19 + 10), 'some text'), - new Quote(new Range(4, 29, 4, 30)), - ]), - new FrontMatterBoolean( - new Word(new Range(4, 34, 4, 34 + 4), 'true'), - ), - new RightBracket(new Range(4, 38, 4, 39)), - ]); - - const record = new FrontMatterRecord([ - recordName, recordDelimiter, recordValue, - ]); - - const trimmed = record.trimValueEnd(); - assert.deepStrictEqual( - trimmed, - [], - 'Must return empty list of trimmed spacing tokens.', - ); - - assert( - record.range.equalsRange( - new Range(4, 10, 4, 39), - ), - 'Must not update token range.', - ); - }); - }); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterSequence.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterSequence.test.ts deleted file mode 100644 index 68602622bba..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/frontMatterDecoder/frontMatterSequence.test.ts +++ /dev/null @@ -1,106 +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 assert from 'assert'; -import { Range } from '../../../../../../../../../editor/common/core/range.js'; -import { FrontMatterValueToken } from '../../../../../../common/promptSyntax/codecs/base/frontMatterCodec/tokens/index.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../../../base/test/common/utils.js'; -import { Space, Tab, VerticalTab, Word } from '../../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/tokens.js'; -import { FrontMatterSequence } from '../../../../../../common/promptSyntax/codecs/base/frontMatterCodec/tokens/frontMatterSequence.js'; - -suite('FrontMatterSequence', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - test('extends \'FrontMatterValueToken\'', () => { - const sequence = new FrontMatterSequence([ - new Word( - new Range(1, 1, 1, 5), - 'test', - ), - ]); - - assert( - sequence instanceof FrontMatterValueToken, - 'Must extend FrontMatterValueToken class.', - ); - }); - - suite('trimEnd()', () => { - test('trims space tokens at the end of the sequence', () => { - const sequence = new FrontMatterSequence([ - new Word(new Range(4, 18, 4, 18 + 10), 'some-value'), - new Space(new Range(4, 28, 4, 29)), - new Space(new Range(4, 29, 4, 30)), - new VerticalTab(new Range(4, 30, 4, 31)), - new Tab(new Range(4, 31, 4, 32)), - new Space(new Range(4, 32, 4, 33)), - ]); - - const trimmed = sequence.trimEnd(); - assert.deepStrictEqual( - trimmed, - [ - new Space(new Range(4, 28, 4, 29)), - new Space(new Range(4, 29, 4, 30)), - new VerticalTab(new Range(4, 30, 4, 31)), - new Tab(new Range(4, 31, 4, 32)), - new Space(new Range(4, 32, 4, 33)), - ], - 'Must return correct trimmed list of spacing tokens.', - ); - - assert( - sequence.range.equalsRange( - new Range(4, 18, 4, 28), - ), - 'Must correctly update token range.', - ); - }); - - test('remains functional if only spacing tokens were present', () => { - const sequence = new FrontMatterSequence([ - new Space(new Range(4, 28, 4, 29)), - new Space(new Range(4, 29, 4, 30)), - new VerticalTab(new Range(4, 30, 4, 31)), - new Tab(new Range(4, 31, 4, 32)), - new Space(new Range(4, 32, 4, 33)), - ]); - - const trimmed = sequence.trimEnd(); - assert.deepStrictEqual( - trimmed, - [ - new Space(new Range(4, 28, 4, 29)), - new Space(new Range(4, 29, 4, 30)), - new VerticalTab(new Range(4, 30, 4, 31)), - new Tab(new Range(4, 31, 4, 32)), - new Space(new Range(4, 32, 4, 33)), - ], - 'Must return correct trimmed list of spacing tokens.', - ); - - assert( - sequence.range.equalsRange( - new Range(4, 28, 4, 28), - ), - 'Must correctly update token range.', - ); - - assert.deepStrictEqual( - sequence.children, - [ - new Word(new Range(4, 28, 4, 28), ''), - ], - 'Must contain a single empty token.', - ); - }); - }); - - test('throws if no tokens provided', () => { - assert.throws(() => { - new FrontMatterSequence([]); - }); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/linesDecoder.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/linesDecoder.test.ts deleted file mode 100644 index b9ac4916923..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/linesDecoder.test.ts +++ /dev/null @@ -1,256 +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 assert from 'assert'; -import { Range } from '../../../../../../../../editor/common/core/range.js'; -import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; -import { DisposableStore } from '../../../../../../../../base/common/lifecycle.js'; -import { Line } from '../../../../../common/promptSyntax/codecs/base/linesCodec/tokens/line.js'; -import { TestDecoder, TTokensConsumeMethod } from './utils/testDecoder.js'; -import { NewLine } from '../../../../../common/promptSyntax/codecs/base/linesCodec/tokens/newLine.js'; -import { newWriteableStream, WriteableStream } from '../../../../../../../../base/common/stream.js'; -import { CarriageReturn } from '../../../../../common/promptSyntax/codecs/base/linesCodec/tokens/carriageReturn.js'; -import { LinesDecoder, TLineToken } from '../../../../../common/promptSyntax/codecs/base/linesCodec/linesDecoder.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../../base/test/common/utils.js'; - -/** - * Note! This decoder is also often used to test common logic of abstract {@link BaseDecoder} - * class, because the {@link LinesDecoder} is one of the simplest non-abstract decoders we have. - */ -suite('LinesDecoder', () => { - const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - - /** - * Test the core logic with specific method of consuming - * tokens that are produced by a lines decoder instance. - */ - suite('core logic', () => { - testLinesDecoder('async-generator', disposables); - testLinesDecoder('consume-all-method', disposables); - testLinesDecoder('on-data-event', disposables); - }); - - suite('settled promise', () => { - test('throws if accessed on not-yet-started decoder instance', () => { - const test = disposables.add(new TestLinesDecoder()); - - assert.throws( - () => { - // testing the field access that throws here, so - // its OK to not use the returned value afterwards - // eslint-disable-next-line local/code-no-unused-expressions - test.decoder.settled; - }, - [ - 'Cannot get `settled` promise of a stream that has not been started.', - 'Please call `start()` first.', - ].join(' '), - ); - }); - }); - - suite('start', () => { - test('throws if the decoder object is already `disposed`', () => { - const test = disposables.add(new TestLinesDecoder()); - const { decoder } = test; - decoder.dispose(); - - assert.throws( - decoder.start.bind(decoder), - 'Cannot start stream that has already disposed.', - ); - }); - - test('throws if the decoder object is already `ended`', async () => { - const inputStream = newWriteableStream(null); - const test = disposables.add(new TestLinesDecoder(inputStream)); - const { decoder } = test; - - setTimeout(() => { - test.sendData([ - 'hello', - 'world :wave:', - ]); - }, 5); - - const receivedTokens = await decoder.start() - .consumeAll(); - - // a basic sanity check for received tokens - assert.strictEqual( - receivedTokens.length, - 3, - 'Must produce the correct number of tokens.', - ); - - // validate that calling `start()` after stream has ended throws - assert.throws( - decoder.start.bind(decoder), - 'Cannot start stream that has already ended.', - ); - }); - }); -}); - - -/** - * A reusable test utility that asserts that a `LinesDecoder` instance - * correctly decodes `inputData` into a stream of `TLineToken` tokens. - * - * ## Examples - * - * ```typescript - * // create a new test utility instance - * const test = disposables.add(new TestLinesDecoder()); - * - * // run the test - * await test.run( - * ' hello world\n', - * [ - * new Line(1, ' hello world'), - * new NewLine(new Range(1, 13, 1, 14)), - * ], - * ); - */ -export class TestLinesDecoder extends TestDecoder { - constructor( - inputStream?: WriteableStream, - ) { - const stream = (inputStream) - ? inputStream - : newWriteableStream(null); - - const decoder = new LinesDecoder(stream); - - super(stream, decoder); - } -} - -/** - * Common reusable test utility to validate {@link LinesDecoder} logic with - * the provided {@link tokensConsumeMethod} way of consuming decoder-produced tokens. - * - * @throws if a test fails, please see thrown error for failure details. - * @param tokensConsumeMethod The way to consume tokens produced by the decoder. - * @param disposables Test disposables store. - */ -function testLinesDecoder( - tokensConsumeMethod: TTokensConsumeMethod, - disposables: Pick, -) { - suite(tokensConsumeMethod, () => { - suite('produces expected tokens', () => { - test('input starts with line data', async () => { - const test = disposables.add(new TestLinesDecoder()); - - await test.run( - ' hello world\nhow are you doing?\n\n 😊 \r', - [ - new Line(1, ' hello world'), - new NewLine(new Range(1, 13, 1, 14)), - new Line(2, 'how are you doing?'), - new NewLine(new Range(2, 19, 2, 20)), - new Line(3, ''), - new NewLine(new Range(3, 1, 3, 2)), - new Line(4, ' 😊 '), - new NewLine(new Range(4, 5, 4, 6)), - ], - ); - }); - - test('standalone \\r is treated as new line', async () => { - const test = disposables.add(new TestLinesDecoder()); - - await test.run( - ' hello world\nhow are you doing?\n\n 😊 \r ', - [ - new Line(1, ' hello world'), - new NewLine(new Range(1, 13, 1, 14)), - new Line(2, 'how are you doing?'), - new NewLine(new Range(2, 19, 2, 20)), - new Line(3, ''), - new NewLine(new Range(3, 1, 3, 2)), - new Line(4, ' 😊 '), - new NewLine(new Range(4, 5, 4, 6)), - new Line(5, ' '), - ], - ); - }); - - test('input starts with a new line', async () => { - const test = disposables.add(new TestLinesDecoder()); - - await test.run( - '\nsome text on this line\n\n\nanother 💬 on this line\r\n🤫\n', - [ - new Line(1, ''), - new NewLine(new Range(1, 1, 1, 2)), - new Line(2, 'some text on this line'), - new NewLine(new Range(2, 23, 2, 24)), - new Line(3, ''), - new NewLine(new Range(3, 1, 3, 2)), - new Line(4, ''), - new NewLine(new Range(4, 1, 4, 2)), - new Line(5, 'another 💬 on this line'), - new CarriageReturn(new Range(5, 24, 5, 25)), - new NewLine(new Range(5, 25, 5, 26)), - new Line(6, '🤫'), - new NewLine(new Range(6, 3, 6, 4)), - ], - ); - }); - - test('input starts and ends with multiple new lines', async () => { - const test = disposables.add(new TestLinesDecoder()); - - await test.run( - '\n\n\r\nciao! 🗯️\t💭 💥 come\tva?\n\n\n\n\n', - [ - new Line(1, ''), - new NewLine(new Range(1, 1, 1, 2)), - new Line(2, ''), - new NewLine(new Range(2, 1, 2, 2)), - new Line(3, ''), - new CarriageReturn(new Range(3, 1, 3, 2)), - new NewLine(new Range(3, 2, 3, 3)), - new Line(4, 'ciao! 🗯️\t💭 💥 come\tva?'), - new NewLine(new Range(4, 25, 4, 26)), - new Line(5, ''), - new NewLine(new Range(5, 1, 5, 2)), - new Line(6, ''), - new NewLine(new Range(6, 1, 6, 2)), - new Line(7, ''), - new NewLine(new Range(7, 1, 7, 2)), - new Line(8, ''), - new NewLine(new Range(8, 1, 8, 2)), - ], - ); - }); - - test('single carriage return is treated as new line', async () => { - const test = disposables.add(new TestLinesDecoder()); - - await test.run( - '\r\rhaalo! 💥💥 how\'re you?\r ?!\r\n\r\n ', - [ - new Line(1, ''), - new NewLine(new Range(1, 1, 1, 2)), - new Line(2, ''), - new NewLine(new Range(2, 1, 2, 2)), - new Line(3, 'haalo! 💥💥 how\'re you?'), - new NewLine(new Range(3, 24, 3, 25)), - new Line(4, ' ?!'), - new CarriageReturn(new Range(4, 4, 4, 5)), - new NewLine(new Range(4, 5, 4, 6)), - new Line(5, ''), - new CarriageReturn(new Range(5, 1, 5, 2)), - new NewLine(new Range(5, 2, 5, 3)), - new Line(6, ' '), - ], - ); - }); - }); - }); -} diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/markdownDecoder.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/markdownDecoder.test.ts deleted file mode 100644 index 37a3331f796..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/codecs/base/markdownDecoder.test.ts +++ /dev/null @@ -1,937 +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 assert from 'assert'; -import { TestDecoder } from './utils/testDecoder.js'; -import { Range } from '../../../../../../../../editor/common/core/range.js'; -import { VSBuffer } from '../../../../../../../../base/common/buffer.js'; -import { newWriteableStream } from '../../../../../../../../base/common/stream.js'; -import { Tab } from '../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/tab.js'; -import { Word } from '../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/word.js'; -import { Dash } from '../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/dash.js'; -import { Space } from '../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/space.js'; -import { Slash } from '../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/slash.js'; -import { NewLine } from '../../../../../common/promptSyntax/codecs/base/linesCodec/tokens/newLine.js'; -import { FormFeed } from '../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/formFeed.js'; -import { VerticalTab } from '../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/verticalTab.js'; -import { MarkdownLink } from '../../../../../common/promptSyntax/codecs/base/markdownCodec/tokens/markdownLink.js'; -import { CarriageReturn } from '../../../../../common/promptSyntax/codecs/base/linesCodec/tokens/carriageReturn.js'; -import { MarkdownImage } from '../../../../../common/promptSyntax/codecs/base/markdownCodec/tokens/markdownImage.js'; -import { ExclamationMark } from '../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/exclamationMark.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../../base/test/common/utils.js'; -import { MarkdownComment } from '../../../../../common/promptSyntax/codecs/base/markdownCodec/tokens/markdownComment.js'; -import { LeftBracket, RightBracket } from '../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/brackets.js'; -import { MarkdownDecoder, TMarkdownToken } from '../../../../../common/promptSyntax/codecs/base/markdownCodec/markdownDecoder.js'; -import { LeftParenthesis, RightParenthesis } from '../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/parentheses.js'; -import { LeftAngleBracket, RightAngleBracket } from '../../../../../common/promptSyntax/codecs/base/simpleCodec/tokens/angleBrackets.js'; - -/** - * A reusable test utility that asserts that a `TestMarkdownDecoder` instance - * correctly decodes `inputData` into a stream of `TMarkdownToken` tokens. - * - * ## Examples - * - * ```typescript - * // create a new test utility instance - * const test = testDisposables.add(new TestMarkdownDecoder()); - * - * // run the test - * await test.run( - * ' hello [world](/etc/hosts)!', - * [ - * new Space(new Range(1, 1, 1, 2)), - * new Word(new Range(1, 2, 1, 7), 'hello'), - * new Space(new Range(1, 7, 1, 8)), - * new MarkdownLink(1, 8, '[world]', '(/etc/hosts)'), - * new Word(new Range(1, 27, 1, 28), '!'), - * new NewLine(new Range(1, 28, 1, 29)), - * ], - * ); - */ -export class TestMarkdownDecoder extends TestDecoder { - constructor() { - const stream = newWriteableStream(null); - - super(stream, new MarkdownDecoder(stream)); - } -} - -suite('MarkdownDecoder', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - - suite('general', () => { - test('base cases', async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - await test.run( - [ - // basic text - ' hello world', - // text with markdown link and special characters in the filename - 'how are\t you [caption text](./some/file/path/refer🎨nce.md)?\v', - // empty line - '', - // markdown link with special characters in the link caption and path - '[(example!)](another/path/with[-and-]-chars/folder)\t ', - // markdown link `#file` variable in the caption and with absolute path - '\t[#file:something.txt](/absolute/path/to/something.txt)', - // text with a commented out markdown link - '\v\f machines must suffer', - ], - [ - // first line - new Space(new Range(1, 1, 1, 2)), - new Word(new Range(1, 2, 1, 7), 'hello'), - new Space(new Range(1, 7, 1, 8)), - new Word(new Range(1, 8, 1, 13), 'world'), - new NewLine(new Range(1, 13, 1, 14)), - // second line - new Word(new Range(2, 1, 2, 4), 'how'), - new Space(new Range(2, 4, 2, 5)), - 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, 13), 'you'), - new Space(new Range(2, 13, 2, 14)), - new MarkdownLink(2, 14, '[caption text]', '(./some/file/path/refer🎨nce.md)'), - new Word(new Range(2, 60, 2, 61), '?'), - new VerticalTab(new Range(2, 61, 2, 62)), - new NewLine(new Range(2, 62, 2, 63)), - // third line - new NewLine(new Range(3, 1, 3, 2)), - // fourth line - new MarkdownLink(4, 1, '[(example!)]', '(another/path/with[-and-]-chars/folder)'), - new Tab(new Range(4, 52, 4, 53)), - new Space(new Range(4, 53, 4, 54)), - new NewLine(new Range(4, 54, 4, 55)), - // fifth line - new Tab(new Range(5, 1, 5, 2)), - new MarkdownLink(5, 2, '[#file:something.txt]', '(/absolute/path/to/something.txt)'), - new NewLine(new Range(5, 56, 5, 57)), - // sixth line - new VerticalTab(new Range(6, 1, 6, 2)), - new FormFeed(new Range(6, 2, 6, 3)), - new Space(new Range(6, 3, 6, 4)), - new Word(new Range(6, 4, 6, 12), 'machines'), - new Space(new Range(6, 12, 6, 13)), - new Word(new Range(6, 13, 6, 17), 'must'), - new Space(new Range(6, 17, 6, 18)), - new MarkdownComment(new Range(6, 18, 6, 18 + 41), ''), - new Space(new Range(6, 59, 6, 60)), - new Word(new Range(6, 60, 6, 66), 'suffer'), - ], - ); - }); - - test('nuanced', async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - const inputLines = [ - // tests that the link caption contain a chat prompt `#file:` reference, while - // the file path can contain other `graphical characters` - '\v\t[#file:./another/path/to/file.txt](./real/file!path/file◆name.md)', - // tests that the link file path contain a chat prompt `#file:` reference, - // `spaces`, `emojies`, and other `graphical characters` - ' [reference ∘ label](/absolute/pa th/to-#file:file.txt/f🥸⚡️le.md)', - // tests that link caption and file path can contain `parentheses`, `spaces`, and - // `emojies` - '\f[!(hello)!](./w(())rld/nice-🦚-filen(a).git))\n\t', - // tests that the link caption can be empty, while the file path can contain `square brackets` - '[](./s[]me/pa[h!) ', - ]; - - await test.run( - inputLines, - [ - // `1st` line - new VerticalTab(new Range(1, 1, 1, 2)), - new Tab(new Range(1, 2, 1, 3)), - new MarkdownLink(1, 3, '[#file:./another/path/to/file.txt]', '(./real/file!path/file◆name.md)'), - new NewLine(new Range(1, 68, 1, 69)), - // `2nd` line - new Space(new Range(2, 1, 2, 2)), - new MarkdownLink(2, 2, '[reference ∘ label]', '(/absolute/pa th/to-#file:file.txt/f🥸⚡️le.md)'), - new NewLine(new Range(2, 67, 2, 68)), - // `3rd` line - new FormFeed(new Range(3, 1, 3, 2)), - new MarkdownLink(3, 2, '[!(hello)!]', '(./w(())rld/nice-🦚-filen(a).git)'), - new RightParenthesis(new Range(3, 50, 3, 51)), - new NewLine(new Range(3, 51, 3, 52)), - // `4th` line - new Tab(new Range(4, 1, 4, 2)), - new NewLine(new Range(4, 2, 4, 3)), - // `5th` line - new MarkdownLink(5, 1, '[]', '(./s[]me/pa[h!)'), - new Space(new Range(5, 24, 5, 25)), - ], - ); - }); - }); - - suite('links', () => { - suite('broken', () => { - test('invalid', async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - const inputLines = [ - // incomplete link reference with empty caption - '[ ](./real/file path/file⇧name.md', - // space between caption and reference is disallowed - '[link text] (./file path/name.txt)', - ]; - - await test.run( - inputLines, - [ - // `1st` line - new LeftBracket(new Range(1, 1, 1, 2)), - new Space(new Range(1, 2, 1, 3)), - new RightBracket(new Range(1, 3, 1, 4)), - new LeftParenthesis(new Range(1, 4, 1, 5)), - new Word(new Range(1, 5, 1, 5 + 1), '.'), - new Slash(new Range(1, 6, 1, 7)), - new Word(new Range(1, 7, 1, 7 + 4), 'real'), - new Slash(new Range(1, 11, 1, 12)), - new Word(new Range(1, 12, 1, 12 + 4), 'file'), - new Space(new Range(1, 16, 1, 17)), - new Word(new Range(1, 17, 1, 17 + 4), 'path'), - new Slash(new Range(1, 21, 1, 22)), - new Word(new Range(1, 22, 1, 22 + 12), 'file⇧name.md'), - new NewLine(new Range(1, 34, 1, 35)), - // `2nd` line - new LeftBracket(new Range(2, 1, 2, 2)), - new Word(new Range(2, 2, 2, 2 + 4), 'link'), - new Space(new Range(2, 6, 2, 7)), - new Word(new Range(2, 7, 2, 7 + 4), 'text'), - new RightBracket(new Range(2, 11, 2, 12)), - new Space(new Range(2, 12, 2, 13)), - new LeftParenthesis(new Range(2, 13, 2, 14)), - new Word(new Range(2, 14, 2, 14 + 1), '.'), - new Slash(new Range(2, 15, 2, 16)), - new Word(new Range(2, 16, 2, 16 + 4), 'file'), - new Space(new Range(2, 20, 2, 21)), - new Word(new Range(2, 21, 2, 21 + 4), 'path'), - new Slash(new Range(2, 25, 2, 26)), - new Word(new Range(2, 26, 2, 26 + 8), 'name.txt'), - new RightParenthesis(new Range(2, 34, 2, 35)), - ], - ); - }); - - suite('stop characters inside caption/reference (new lines)', () => { - for (const StopCharacter of [CarriageReturn, NewLine]) { - let characterName = ''; - - if (StopCharacter === CarriageReturn) { - characterName = '\\r'; - } - if (StopCharacter === NewLine) { - characterName = '\\n'; - } - - assert( - characterName !== '', - 'The "characterName" must be set, got "empty line".', - ); - - test(`stop character - "${characterName}"`, async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - const inputLines = [ - // stop character inside link caption - `[haa${StopCharacter.symbol}loů](./real/💁/name.txt)`, - // stop character inside link reference - `[ref text](/etc/pat${StopCharacter.symbol}h/to/file.md)`, - // stop character between line caption and link reference is disallowed - `[text]${StopCharacter.symbol}(/etc/ path/main.mdc)`, - ]; - - - await test.run( - inputLines, - [ - // `1st` input line - new LeftBracket(new Range(1, 1, 1, 2)), - new Word(new Range(1, 2, 1, 2 + 3), 'haa'), - new NewLine(new Range(1, 5, 1, 6)), // a single CR token is treated as a `new line` - new Word(new Range(2, 1, 2, 1 + 3), 'loů'), - new RightBracket(new Range(2, 4, 2, 5)), - new LeftParenthesis(new Range(2, 5, 2, 6)), - new Word(new Range(2, 6, 2, 6 + 1), '.'), - new Slash(new Range(2, 7, 2, 8)), - new Word(new Range(2, 8, 2, 8 + 4), 'real'), - new Slash(new Range(2, 12, 2, 13)), - new Word(new Range(2, 13, 2, 13 + 2), '💁'), - new Slash(new Range(2, 15, 2, 16)), - new Word(new Range(2, 16, 2, 16 + 8), 'name.txt'), - new RightParenthesis(new Range(2, 24, 2, 25)), - new NewLine(new Range(2, 25, 2, 26)), - // `2nd` input line - new LeftBracket(new Range(3, 1, 3, 2)), - new Word(new Range(3, 2, 3, 2 + 3), 'ref'), - new Space(new Range(3, 5, 3, 6)), - new Word(new Range(3, 6, 3, 6 + 4), 'text'), - new RightBracket(new Range(3, 10, 3, 11)), - new LeftParenthesis(new Range(3, 11, 3, 12)), - new Slash(new Range(3, 12, 3, 13)), - new Word(new Range(3, 13, 3, 13 + 3), 'etc'), - new Slash(new Range(3, 16, 3, 17)), - new Word(new Range(3, 17, 3, 17 + 3), 'pat'), - new NewLine(new Range(3, 20, 3, 21)), // a single CR token is treated as a `new line` - new Word(new Range(4, 1, 4, 1 + 1), 'h'), - new Slash(new Range(4, 2, 4, 3)), - new Word(new Range(4, 3, 4, 3 + 2), 'to'), - new Slash(new Range(4, 5, 4, 6)), - new Word(new Range(4, 6, 4, 6 + 7), 'file.md'), - new RightParenthesis(new Range(4, 13, 4, 14)), - new NewLine(new Range(4, 14, 4, 15)), - // `3nd` input line - new LeftBracket(new Range(5, 1, 5, 2)), - new Word(new Range(5, 2, 5, 2 + 4), 'text'), - new RightBracket(new Range(5, 6, 5, 7)), - new NewLine(new Range(5, 7, 5, 8)), // a single CR token is treated as a `new line` - new LeftParenthesis(new Range(6, 1, 6, 2)), - new Slash(new Range(6, 2, 6, 3)), - new Word(new Range(6, 3, 6, 3 + 3), 'etc'), - new Slash(new Range(6, 6, 6, 7)), - new Space(new Range(6, 7, 6, 8)), - new Word(new Range(6, 8, 6, 8 + 4), 'path'), - new Slash(new Range(6, 12, 6, 13)), - new Word(new Range(6, 13, 6, 13 + 8), 'main.mdc'), - new RightParenthesis(new Range(6, 21, 6, 22)), - ], - ); - }); - } - }); - - /** - * Same as above but these stop characters do not move the caret to the next line. - */ - suite('stop characters inside caption/reference (same line)', () => { - for (const StopCharacter of [VerticalTab, FormFeed]) { - let characterName = ''; - - if (StopCharacter === VerticalTab) { - characterName = '\\v'; - } - if (StopCharacter === FormFeed) { - characterName = '\\f'; - } - - assert( - characterName !== '', - 'The "characterName" must be set, got "empty line".', - ); - - test(`stop character - "${characterName}"`, async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - const inputLines = [ - // stop character inside link caption - `[haa${StopCharacter.symbol}loů](./real/💁/name.txt)`, - // stop character inside link reference - `[ref text](/etc/pat${StopCharacter.symbol}h/to/file.md)`, - // stop character between line caption and link reference is disallowed - `[text]${StopCharacter.symbol}(/etc/ path/file.md)`, - ]; - - - await test.run( - inputLines, - [ - // `1st` input line - new LeftBracket(new Range(1, 1, 1, 2)), - new Word(new Range(1, 2, 1, 2 + 3), 'haa'), - new StopCharacter(new Range(1, 5, 1, 6)), // <- stop character - new Word(new Range(1, 6, 1, 6 + 3), 'loů'), - new RightBracket(new Range(1, 9, 1, 10)), - new LeftParenthesis(new Range(1, 10, 1, 11)), - new Word(new Range(1, 11, 1, 11 + 1), '.'), - new Slash(new Range(1, 12, 1, 13)), - new Word(new Range(1, 13, 1, 13 + 4), 'real'), - new Slash(new Range(1, 17, 1, 18)), - new Word(new Range(1, 18, 1, 18 + 2), '💁'), - new Slash(new Range(1, 20, 1, 21)), - new Word(new Range(1, 21, 1, 21 + 8), 'name.txt'), - new RightParenthesis(new Range(1, 29, 1, 30)), - new NewLine(new Range(1, 30, 1, 31)), - // `2nd` input line - new LeftBracket(new Range(2, 1, 2, 2)), - new Word(new Range(2, 2, 2, 2 + 3), 'ref'), - new Space(new Range(2, 5, 2, 6)), - new Word(new Range(2, 6, 2, 6 + 4), 'text'), - new RightBracket(new Range(2, 10, 2, 11)), - new LeftParenthesis(new Range(2, 11, 2, 12)), - new Slash(new Range(2, 12, 2, 13)), - new Word(new Range(2, 13, 2, 13 + 3), 'etc'), - new Slash(new Range(2, 16, 2, 17)), - new Word(new Range(2, 17, 2, 17 + 3), 'pat'), - new StopCharacter(new Range(2, 20, 2, 21)), // <- stop character - new Word(new Range(2, 21, 2, 21 + 1), 'h'), - new Slash(new Range(2, 22, 2, 23)), - new Word(new Range(2, 23, 2, 23 + 2), 'to'), - new Slash(new Range(2, 25, 2, 26)), - new Word(new Range(2, 26, 2, 26 + 7), 'file.md'), - new RightParenthesis(new Range(2, 33, 2, 34)), - new NewLine(new Range(2, 34, 2, 35)), - // `3nd` input line - new LeftBracket(new Range(3, 1, 3, 2)), - new Word(new Range(3, 2, 3, 2 + 4), 'text'), - new RightBracket(new Range(3, 6, 3, 7)), - new StopCharacter(new Range(3, 7, 3, 8)), // <- stop character - new LeftParenthesis(new Range(3, 8, 3, 9)), - new Slash(new Range(3, 9, 3, 10)), - new Word(new Range(3, 10, 3, 10 + 3), 'etc'), - new Slash(new Range(3, 13, 3, 14)), - new Space(new Range(3, 14, 3, 15)), - new Word(new Range(3, 15, 3, 15 + 4), 'path'), - new Slash(new Range(3, 19, 3, 20)), - new Word(new Range(3, 20, 3, 20 + 7), 'file.md'), - new RightParenthesis(new Range(3, 27, 3, 28)), - ], - ); - }); - } - }); - }); - }); - - - suite('images', () => { - suite('general', () => { - test('base cases', async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - const inputData = [ - '\t![alt text](./some/path/to/file.jpg) ', - 'plain text \f![label](./image.png)\v and more text', - '![](/var/images/default) following text', - ]; - - await test.run( - inputData, - [ - // `1st` - new Tab(new Range(1, 1, 1, 2)), - new MarkdownImage(1, 2, '![alt text]', '(./some/path/to/file.jpg)'), - new Space(new Range(1, 38, 1, 39)), - new NewLine(new Range(1, 39, 1, 40)), - // `2nd` - new Word(new Range(2, 1, 2, 6), 'plain'), - new Space(new Range(2, 6, 2, 7)), - new Word(new Range(2, 7, 2, 11), 'text'), - new Space(new Range(2, 11, 2, 12)), - new FormFeed(new Range(2, 12, 2, 13)), - new MarkdownImage(2, 13, '![label]', '(./image.png)'), - new VerticalTab(new Range(2, 34, 2, 35)), - new Space(new Range(2, 35, 2, 36)), - new Word(new Range(2, 36, 2, 39), 'and'), - new Space(new Range(2, 39, 2, 40)), - new Word(new Range(2, 40, 2, 44), 'more'), - new Space(new Range(2, 44, 2, 45)), - new Word(new Range(2, 45, 2, 49), 'text'), - new NewLine(new Range(2, 49, 2, 50)), - // `3rd` - new MarkdownImage(3, 1, '![]', '(/var/images/default)'), - new Space(new Range(3, 25, 3, 26)), - new Word(new Range(3, 26, 3, 35), 'following'), - new Space(new Range(3, 35, 3, 36)), - new Word(new Range(3, 36, 3, 40), 'text'), - ], - ); - }); - - test('nuanced', async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - const inputData = [ - '\t![](./s☻me/path/to/file.jpeg) ', - 'raw text \f![(/1.png)](./image-🥸.png)\v and more text', - // '![](/var/images/default) following text', - ]; - - await test.run( - inputData, - [ - // `1st` - new Tab(new Range(1, 1, 1, 2)), - new MarkdownImage(1, 2, '![]', '(./s☻me/path/to/file.jpeg)'), - new Space(new Range(1, 47, 1, 48)), - new NewLine(new Range(1, 48, 1, 49)), - // `2nd` - new Word(new Range(2, 1, 2, 4), 'raw'), - new Space(new Range(2, 4, 2, 5)), - new Word(new Range(2, 5, 2, 9), 'text'), - new Space(new Range(2, 9, 2, 10)), - new FormFeed(new Range(2, 10, 2, 11)), - new MarkdownImage(2, 11, '![(/1.png)]', '(./image-🥸.png)'), - new VerticalTab(new Range(2, 38, 2, 39)), - new Space(new Range(2, 39, 2, 40)), - new Word(new Range(2, 40, 2, 43), 'and'), - new Space(new Range(2, 43, 2, 44)), - new Word(new Range(2, 44, 2, 48), 'more'), - new Space(new Range(2, 48, 2, 49)), - new Word(new Range(2, 49, 2, 53), 'text'), - ], - ); - }); - }); - - suite('broken', () => { - test('invalid', async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - const inputLines = [ - // incomplete link reference with empty caption - '![ ](./real/file path/file★name.webp', - // space between caption and reference is disallowed - '\f![link text] (./file path/name.jpg)', - // new line inside the link reference - '\v![ ](./file\npath/name.jpeg )', - ]; - - await test.run( - inputLines, - [ - // `1st` line - new ExclamationMark(new Range(1, 1, 1, 2)), - new LeftBracket(new Range(1, 2, 1, 3)), - new Space(new Range(1, 3, 1, 4)), - new RightBracket(new Range(1, 4, 1, 5)), - new LeftParenthesis(new Range(1, 5, 1, 6)), - new Word(new Range(1, 6, 1, 6 + 1), '.'), - new Slash(new Range(1, 7, 1, 8)), - new Word(new Range(1, 8, 1, 8 + 4), 'real'), - new Slash(new Range(1, 12, 1, 13)), - new Word(new Range(1, 13, 1, 13 + 4), 'file'), - new Space(new Range(1, 17, 1, 18)), - new Word(new Range(1, 18, 1, 18 + 4), 'path'), - new Slash(new Range(1, 22, 1, 23)), - new Word(new Range(1, 23, 1, 23 + 14), 'file★name.webp'), - new NewLine(new Range(1, 37, 1, 38)), - // `2nd` line - new FormFeed(new Range(2, 1, 2, 2)), - new ExclamationMark(new Range(2, 2, 2, 3)), - new LeftBracket(new Range(2, 3, 2, 4)), - new Word(new Range(2, 4, 2, 4 + 4), 'link'), - new Space(new Range(2, 8, 2, 9)), - new Word(new Range(2, 9, 2, 9 + 4), 'text'), - new RightBracket(new Range(2, 13, 2, 14)), - new Space(new Range(2, 14, 2, 15)), - new LeftParenthesis(new Range(2, 15, 2, 16)), - new Word(new Range(2, 16, 2, 16 + 1), '.'), - new Slash(new Range(2, 17, 2, 18)), - new Word(new Range(2, 18, 2, 18 + 4), 'file'), - new Space(new Range(2, 22, 2, 23)), - new Word(new Range(2, 23, 2, 23 + 4), 'path'), - new Slash(new Range(2, 27, 2, 28)), - new Word(new Range(2, 28, 2, 28 + 8), 'name.jpg'), - new RightParenthesis(new Range(2, 36, 2, 37)), - new NewLine(new Range(2, 37, 2, 38)), - // `3rd` line - new VerticalTab(new Range(3, 1, 3, 2)), - new ExclamationMark(new Range(3, 2, 3, 3)), - new LeftBracket(new Range(3, 3, 3, 4)), - new Space(new Range(3, 4, 3, 5)), - new RightBracket(new Range(3, 5, 3, 6)), - new LeftParenthesis(new Range(3, 6, 3, 7)), - new Word(new Range(3, 7, 3, 7 + 1), '.'), - new Slash(new Range(3, 8, 3, 9)), - new Word(new Range(3, 9, 3, 9 + 4), 'file'), - new NewLine(new Range(3, 13, 3, 14)), - new Word(new Range(4, 1, 4, 1 + 4), 'path'), - new Slash(new Range(4, 5, 4, 6)), - new Word(new Range(4, 6, 4, 6 + 9), 'name.jpeg'), - new Space(new Range(4, 15, 4, 16)), - new RightParenthesis(new Range(4, 16, 4, 17)), - ], - ); - }); - - suite('stop characters inside caption/reference (new lines)', () => { - for (const StopCharacter of [CarriageReturn, NewLine]) { - let characterName = ''; - - if (StopCharacter === CarriageReturn) { - characterName = '\\r'; - } - if (StopCharacter === NewLine) { - characterName = '\\n'; - } - - assert( - characterName !== '', - 'The "characterName" must be set, got "empty line".', - ); - - test(`stop character - "${characterName}"`, async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - const inputLines = [ - // stop character inside link caption - `![haa${StopCharacter.symbol}loů](./real/💁/name.png)`, - // stop character inside link reference - `![ref text](/etc/pat${StopCharacter.symbol}h/to/file.webp)`, - // stop character between line caption and link reference is disallowed - `![text]${StopCharacter.symbol}(/etc/ path/file.jpeg)`, - ]; - - - await test.run( - inputLines, - [ - // `1st` input line - new ExclamationMark(new Range(1, 1, 1, 2)), - new LeftBracket(new Range(1, 2, 1, 3)), - new Word(new Range(1, 3, 1, 3 + 3), 'haa'), - new NewLine(new Range(1, 6, 1, 7)), // a single CR token is treated as a `new line` - new Word(new Range(2, 1, 2, 1 + 3), 'loů'), - new RightBracket(new Range(2, 4, 2, 5)), - new LeftParenthesis(new Range(2, 5, 2, 6)), - new Word(new Range(2, 6, 2, 6 + 1), '.'), - new Slash(new Range(2, 7, 2, 8)), - new Word(new Range(2, 8, 2, 8 + 4), 'real'), - new Slash(new Range(2, 12, 2, 13)), - new Word(new Range(2, 13, 2, 13 + 2), '💁'), - new Slash(new Range(2, 15, 2, 16)), - new Word(new Range(2, 16, 2, 16 + 8), 'name.png'), - new RightParenthesis(new Range(2, 24, 2, 25)), - new NewLine(new Range(2, 25, 2, 26)), - // `2nd` input line - new ExclamationMark(new Range(3, 1, 3, 2)), - new LeftBracket(new Range(3, 2, 3, 3)), - new Word(new Range(3, 3, 3, 3 + 3), 'ref'), - new Space(new Range(3, 6, 3, 7)), - new Word(new Range(3, 7, 3, 7 + 4), 'text'), - new RightBracket(new Range(3, 11, 3, 12)), - new LeftParenthesis(new Range(3, 12, 3, 13)), - new Slash(new Range(3, 13, 3, 14)), - new Word(new Range(3, 14, 3, 14 + 3), 'etc'), - new Slash(new Range(3, 17, 3, 18)), - new Word(new Range(3, 18, 3, 18 + 3), 'pat'), - new NewLine(new Range(3, 21, 3, 22)), // a single CR token is treated as a `new line` - new Word(new Range(4, 1, 4, 1 + 1), 'h'), - new Slash(new Range(4, 2, 4, 3)), - new Word(new Range(4, 3, 4, 3 + 2), 'to'), - new Slash(new Range(4, 5, 4, 6)), - new Word(new Range(4, 6, 4, 6 + 9), 'file.webp'), - new RightParenthesis(new Range(4, 15, 4, 16)), - new NewLine(new Range(4, 16, 4, 17)), - // `3nd` input line - new ExclamationMark(new Range(5, 1, 5, 2)), - new LeftBracket(new Range(5, 2, 5, 3)), - new Word(new Range(5, 3, 5, 3 + 4), 'text'), - new RightBracket(new Range(5, 7, 5, 8)), - new NewLine(new Range(5, 8, 5, 9)), // a single CR token is treated as a `new line` - new LeftParenthesis(new Range(6, 1, 6, 2)), - new Slash(new Range(6, 2, 6, 3)), - new Word(new Range(6, 3, 6, 3 + 3), 'etc'), - new Slash(new Range(6, 6, 6, 7)), - new Space(new Range(6, 7, 6, 8)), - new Word(new Range(6, 8, 6, 8 + 4), 'path'), - new Slash(new Range(6, 12, 6, 13)), - new Word(new Range(6, 13, 6, 13 + 9), 'file.jpeg'), - new RightParenthesis(new Range(6, 22, 6, 23)), - ], - ); - }); - } - }); - - /** - * Same as above but these stop characters do not move the caret to the next line. - */ - suite('stop characters inside caption/reference (same line)', () => { - for (const stopCharacter of [VerticalTab, FormFeed]) { - let characterName = ''; - - if (stopCharacter === VerticalTab) { - characterName = '\\v'; - } - if (stopCharacter === FormFeed) { - characterName = '\\f'; - } - - assert( - characterName !== '', - 'The "characterName" must be set, got "empty line".', - ); - - test(`stop character - "${characterName}"`, async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - const inputLines = [ - // stop character inside link caption - `![haa${stopCharacter.symbol}loů](./real/💁/name)`, - // stop character inside link reference - `![ref text](/etc/pat${stopCharacter.symbol}h/to/file.webp)`, - // stop character between line caption and link reference is disallowed - `![text]${stopCharacter.symbol}(/etc/ path/image.gif)`, - ]; - - - await test.run( - inputLines, - [ - // `1st` input line - new ExclamationMark(new Range(1, 1, 1, 2)), - new LeftBracket(new Range(1, 2, 1, 3)), - new Word(new Range(1, 3, 1, 3 + 3), 'haa'), - new stopCharacter(new Range(1, 6, 1, 7)), // <- stop character - new Word(new Range(1, 7, 1, 7 + 3), 'loů'), - new RightBracket(new Range(1, 10, 1, 11)), - new LeftParenthesis(new Range(1, 11, 1, 12)), - new Word(new Range(1, 12, 1, 12 + 1), '.'), - new Slash(new Range(1, 13, 1, 14)), - new Word(new Range(1, 14, 1, 14 + 4), 'real'), - new Slash(new Range(1, 18, 1, 19)), - new Word(new Range(1, 19, 1, 19 + 2), '💁'), - new Slash(new Range(1, 21, 1, 22)), - new Word(new Range(1, 22, 1, 22 + 4), 'name'), - new RightParenthesis(new Range(1, 26, 1, 27)), - new NewLine(new Range(1, 27, 1, 28)), - // `2nd` input line - new ExclamationMark(new Range(2, 1, 2, 2)), - new LeftBracket(new Range(2, 2, 2, 3)), - new Word(new Range(2, 3, 2, 3 + 3), 'ref'), - new Space(new Range(2, 6, 2, 7)), - new Word(new Range(2, 7, 2, 7 + 4), 'text'), - new RightBracket(new Range(2, 11, 2, 12)), - new LeftParenthesis(new Range(2, 12, 2, 13)), - new Slash(new Range(2, 13, 2, 14)), - new Word(new Range(2, 14, 2, 14 + 3), 'etc'), - new Slash(new Range(2, 17, 2, 18)), - new Word(new Range(2, 18, 2, 18 + 3), 'pat'), - new stopCharacter(new Range(2, 21, 2, 22)), // <- stop character - new Word(new Range(2, 22, 2, 22 + 1), 'h'), - new Slash(new Range(2, 23, 2, 24)), - new Word(new Range(2, 24, 2, 24 + 2), 'to'), - new Slash(new Range(2, 26, 2, 27)), - new Word(new Range(2, 27, 2, 27 + 9), 'file.webp'), - new RightParenthesis(new Range(2, 36, 2, 37)), - new NewLine(new Range(2, 37, 2, 38)), - // `3nd` input line - new ExclamationMark(new Range(3, 1, 3, 2)), - new LeftBracket(new Range(3, 2, 3, 3)), - new Word(new Range(3, 3, 3, 3 + 4), 'text'), - new RightBracket(new Range(3, 7, 3, 8)), - new stopCharacter(new Range(3, 8, 3, 9)), // <- stop character - new LeftParenthesis(new Range(3, 9, 3, 10)), - new Slash(new Range(3, 10, 3, 11)), - new Word(new Range(3, 11, 3, 11 + 3), 'etc'), - new Slash(new Range(3, 14, 3, 15)), - new Space(new Range(3, 15, 3, 16)), - new Word(new Range(3, 16, 3, 16 + 4), 'path'), - new Slash(new Range(3, 20, 3, 21)), - new Word(new Range(3, 21, 3, 21 + 9), 'image.gif'), - new RightParenthesis(new Range(3, 30, 3, 31)), - ], - ); - }); - } - }); - }); - }); - - suite('comments', () => { - suite('general', () => { - test('base cases', async () => { - const test = testDisposables.add( - new TestMarkdownDecoder(), - ); - - const inputData = [ - // comment with text inside it - '\t', - // comment with a link inside - 'some text and more text ', - // comment new lines inside it - ' usual text follows', - // an empty comment - '\t\t', - // comment that was not closed properly - 'haalo\t'), - new NewLine(new Range(1, 22, 1, 23)), - // `2nd` - new Word(new Range(2, 1, 2, 5), 'some'), - new Space(new Range(2, 5, 2, 6)), - new Word(new Range(2, 6, 2, 10), 'text'), - new MarkdownComment(new Range(2, 10, 2, 10 + 46), ''), - new Space(new Range(2, 56, 2, 57)), - new Word(new Range(2, 57, 2, 60), 'and'), - new Space(new Range(2, 60, 2, 61)), - new Word(new Range(2, 61, 2, 65), 'more'), - new Space(new Range(2, 65, 2, 66)), - new Word(new Range(2, 66, 2, 70), 'text'), - new Space(new Range(2, 70, 2, 71)), - new NewLine(new Range(2, 71, 2, 72)), - // `3rd` - new MarkdownComment(new Range(3, 1, 3 + 3, 1 + 13), ''), - new Space(new Range(6, 14, 6, 15)), - new Word(new Range(6, 15, 6, 15 + 5), 'usual'), - new Space(new Range(6, 20, 6, 21)), - new Word(new Range(6, 21, 6, 21 + 4), 'text'), - new Space(new Range(6, 25, 6, 26)), - new Word(new Range(6, 26, 6, 26 + 7), 'follows'), - new NewLine(new Range(6, 33, 6, 34)), - // `4rd` - new Tab(new Range(7, 1, 7, 2)), - new MarkdownComment(new Range(7, 2, 7, 2 + 7), ''), - new Tab(new Range(7, 9, 7, 10)), - new NewLine(new Range(7, 10, 7, 11)), - // `5th` - new Word(new Range(8, 1, 8, 6), 'haalo'), - new Tab(new Range(8, 6, 8, 7)), - new MarkdownComment(new Range(8, 7, 8, 7 + 40), '>', - // comment contains `<[]>` brackets and `!` - '\t\t', - // comment contains `\t\t', - // comment contains `'), - new RightAngleBracket(new Range(1, 19, 1, 20)), - new NewLine(new Range(1, 20, 1, 21)), - // `2nd` - new MarkdownComment(new Range(2, 1, 2, 1 + 21), ''), - new Tab(new Range(2, 22, 2, 23)), - new Tab(new Range(2, 23, 2, 24)), - new NewLine(new Range(2, 24, 2, 25)), - // `3rd` - new VerticalTab(new Range(3, 1, 3, 2)), - new MarkdownComment(new Range(3, 2, 3 + 3, 1 + 7), ''), - new Tab(new Range(6, 8, 6, 9)), - new Tab(new Range(6, 9, 6, 10)), - new NewLine(new Range(6, 10, 6, 11)), - // `4rd` - new Space(new Range(7, 1, 7, 2)), - // note! comment does not have correct closing `-->`, hence the comment extends - // to the end of the text, and therefore includes the \t\v\f and space at the end - new MarkdownComment(new Range(7, 2, 8, 1 + 12), ' ', - ' < !-- світ -->\t', - '\v\f', - '`, hence the comment extends - // to the end of the text, and therefore includes the `space` at the end - new MarkdownComment(new Range(4, 1, 4, 1 + 15), '